天天看點

記APP實作多語言(國際化)過程,相容Android 8.0以上多語言開發過程中的遇到的問題

此文屬于finddreams的原創部落格,轉載請注明出處:http://blog.csdn.net/finddreams/article/details/78470768

  APP為什麼要做多語言?

  首先如果APP的使用者量超級多,并且不隻在内地使用,海外也有市場。那麼來自各個不同國家和地區的人使用的時候,肯定想把這個APP設定成他所熟悉的語言,比如微信,微網誌,支付寶等這些APP都支援多語言設定的。

  此外還有一些股票類的APP,因為股票類的APP所提供的行情報價服務包含了各大證券市場的,有内地的上證指數,深圳指數,還有香港的恒生指數,以及美股的納斯達克和道瓊斯指數等等。這些不同市場的指數涵蓋了中文簡體和繁體,還有英文的使用人群。是以股票類APP為了更符合當地人的語言習慣,多語言(國際化)則是必然要做的事情。這類APP有大智慧,同花順,富途牛牛等。

  筆者所在公司的産品也是做股票類的,主要面向的使用者是香港地區,是以支援繁體和英文多語言切換也是剛需了。

  說到要做多語言,剛開始我們原生這邊就覺得這不是Android系統自帶就支援的東西嗎?在res資源目錄下建立不同語言的values檔案名,Android系統就會自動的去找對應的語言包下的string顯示出來,這應該不難吧?

  于是在網上看了一些教程之後,就開始幹了起來。結果在實際的開發過程中遇到了很多的問題,比如說Android7.0 的相容性問題,以及在試用微信的Tinker熱修複中遇到語言切換失效的問題等。接下來就來談一談筆者在APP多語言過程中的一些經驗分享,希望能夠幫到大家。

                           示例項目的效果圖

              

記APP實作多語言(國際化)過程,相容Android 8.0以上多語言開發過程中的遇到的問題

APP多語言需要原生和背景都要做改動

  背景的話,需要跟随用戶端的語言來傳回對應的語言資料,比如在用戶端設定成簡體的情況下,傳回簡體的資料,繁體的情況,則傳回繁體資料。這就需要用戶端在每次初始化和切換語言之後把請求接口中的語言參數改成對應的語言code,背景根據用戶端的請求中所帶的語言參數來決定傳回什麼樣的語言。背景傳回的語言資料,除了某些特别的詞語為了符合本地的語言習慣,手動配置的,其他的簡繁體詞語,英文都是通過機器翻譯的。

重點說一下原生Android的多語言的做法

  1. 因為以前我們的字元串都是寫死在Java代碼中或者layout布局檔案中的,這樣的話就不可以根據多語言設定來變化,同時也比較難維護。

  是以首先我們要把Java代碼和layout布局檔案中的寫死的字元串寫死抽取出來,統一的放到values/strings.xml的檔案裡面,這樣就能為不同語言的values檔案的strings.xml打下基礎,這是前提條件。這也告訴我們在平時的開發中要注意到這一點,養成把字元串配置在strings.xml檔案裡面的習慣,維護起來友善,将來如果要做多語言的話也不用費時費力的把那些寫死的字元串抽出來了,省了很多無腦的操作。

  2. 建立一個你需要支援的語言包資源檔案,比如values目錄可以放預設的中文簡體strings.xml檔案,values-en則是放置英文版的strings,values-zh-rHK和values-zh-tTW這兩個資源檔案夾分别代表香港和台灣,台灣預設的語言是繁體,香港也有繁體的使用習慣,是以這兩個檔案裡面放的strings.xml幾乎都是一樣的,但其實地方不一樣,本地的語言習慣也是有一些差別的。比如我們寫的部落格用香港的繁體寫也是部落格,但是台灣人習慣稱之為部落格(blog)。

res下的多語言資源檔案如下圖:

記APP實作多語言(國際化)過程,相容Android 8.0以上多語言開發過程中的遇到的問題

  3. 多語言資源包配置好了之後,就是語言切換的代碼編寫了,

package com.finddreams.languagelib;

import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;


import org.greenrobot.eventbus.EventBus;

import java.util.Locale;

/**
 * 多語言切換的幫助類
 * http://blog.csdn.net/finddreams
 */
public class MultiLanguageUtil {

    private static final String TAG = "MultiLanguageUtil";
    private static MultiLanguageUtil instance;
    private Context mContext;
    public static final String SAVE_LANGUAGE = "save_language";

    public static void init(Context mContext) {
        if (instance == null) {
            synchronized (MultiLanguageUtil.class) {
                if (instance == null) {
                    instance = new MultiLanguageUtil(mContext);
                }
            }
        }
    }

    public static MultiLanguageUtil getInstance() {
        if (instance == null) {
            throw new IllegalStateException("You must be init MultiLanguageUtil first");
        }
        return instance;
    }

    private MultiLanguageUtil(Context context) {
        this.mContext = context;
    }

    /**
     * 設定語言
     */
    public void setConfiguration() {
        Locale targetLocale = getLanguageLocale();
        Configuration configuration = mContext.getResources().getConfiguration();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            configuration.setLocale(targetLocale);
        } else {
            configuration.locale = targetLocale;
        }
        Resources resources = mContext.getResources();
        DisplayMetrics dm = resources.getDisplayMetrics();
        resources.updateConfiguration(configuration, dm);//語言更換生效的代碼!
    }

    /**
     * 如果不是英文、簡體中文、繁體中文,預設傳回簡體中文
     *
     * @return
     */
    private Locale getLanguageLocale() {
        int languageType = CommSharedUtil.getInstance(mContext).getInt(MultiLanguageUtil.SAVE_LANGUAGE, );
        if (languageType == LanguageType.LANGUAGE_FOLLOW_SYSTEM) {
            Locale sysType = getSysLocale();
            if (sysType.equals(Locale.ENGLISH)) {
                return Locale.ENGLISH;
            } else if (sysType.equals(Locale.TRADITIONAL_CHINESE)) {
                return Locale.TRADITIONAL_CHINESE;
            } else if (TextUtils.equals(sysType.getLanguage(), Locale.CHINA.getLanguage())) { //zh
                if (TextUtils.equals(sysType.getCountry(), Locale.CHINA.getCountry())) {  //适配華為mate9  zh_CN_#Hans
                    return Locale.SIMPLIFIED_CHINESE;
                }
            } else {
                return Locale.SIMPLIFIED_CHINESE;
            }
        } else if (languageType == LanguageType.LANGUAGE_EN) {
            return Locale.ENGLISH;
        } else if (languageType == LanguageType.LANGUAGE_CHINESE_SIMPLIFIED) {
            return Locale.SIMPLIFIED_CHINESE;
        } else if (languageType == LanguageType.LANGUAGE_CHINESE_TRADITIONAL) {
            return Locale.TRADITIONAL_CHINESE;
        }
        Log.e(TAG, "getLanguageLocale" + languageType + languageType);
        getSystemLanguage(getSysLocale());
        return Locale.SIMPLIFIED_CHINESE;
    }

    private String getSystemLanguage(Locale locale) {
        return locale.getLanguage() + "_" + locale.getCountry();

    }

    //7.0以上擷取方式需要特殊處理一下
    public Locale getSysLocale() {
        if (Build.VERSION.SDK_INT < ) {
            return mContext.getResources().getConfiguration().locale;
        } else {
            return mContext.getResources().getConfiguration().getLocales().get();
        }
    }

    /**
     * 更新語言
     *
     * @param languageType
     */
    public void updateLanguage(int languageType) {
        CommSharedUtil.getInstance(mContext).putInt(MultiLanguageUtil.SAVE_LANGUAGE, languageType);
        MultiLanguageUtil.getInstance().setConfiguration();
        EventBus.getDefault().post(new OnChangeLanguageEvent(languageType));
    }

    public String getLanguageName(Context context) {
        int languageType = CommSharedUtil.getInstance(context).getInt(MultiLanguageUtil.SAVE_LANGUAGE,LanguageType.LANGUAGE_FOLLOW_SYSTEM);
        if (languageType == LanguageType.LANGUAGE_EN) {
            return mContext.getString(R.string.setting_language_english);
        } else if (languageType == LanguageType.LANGUAGE_CHINESE_SIMPLIFIED) {
            return mContext.getString(R.string.setting_simplified_chinese);
        } else if (languageType == LanguageType.LANGUAGE_CHINESE_TRADITIONAL) {
            return mContext.getString(R.string.setting_traditional_chinese);
        }
        return mContext.getString(R.string.setting_language_auto);
    }

    /**
     * 擷取到使用者儲存的語言類型
     * @return
     */
    public int getLanguageType() {
        int languageType = CommSharedUtil.getInstance(mContext).getInt(MultiLanguageUtil.SAVE_LANGUAGE, LanguageType.LANGUAGE_FOLLOW_SYSTEM);
         if (languageType == LanguageType.LANGUAGE_CHINESE_SIMPLIFIED) {
            return LanguageType.LANGUAGE_CHINESE_SIMPLIFIED;
        } else if (languageType == LanguageType.LANGUAGE_CHINESE_TRADITIONAL) {
            return LanguageType.LANGUAGE_CHINESE_TRADITIONAL;
        } else if (languageType == LanguageType.LANGUAGE_FOLLOW_SYSTEM) {
           return LanguageType.LANGUAGE_FOLLOW_SYSTEM;
        }
        Log.e(TAG, "getLanguageType" + languageType);
        return languageType;
    }
}
           

  上面最關鍵的切換語言的代碼是這個setConfiguration()是切換到對應的語言環境的方法,方法中通過Configuration對象來把語言對象locale設定進去,最後通過Resources對象來實作語言的更換生效。

/**
     * 設定語言
     */
    public void setConfiguration() {
        Locale targetLocale = getLanguageLocale();
        Configuration configuration = mContext.getResources().getConfiguration();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            configuration.setLocale(targetLocale);
        } else {
            configuration.locale = targetLocale;
        }
        Resources resources = mContext.getResources();
        DisplayMetrics dm = resources.getDisplayMetrics();
        resources.updateConfiguration(configuration, dm);//語言更換生效的代碼!

    }
           

  getLanguageLocale()方法先擷取到使用者儲存在手機中的語言是什麼,如果沒有設定過則是預設跟随系統的語言設定來,getSysLocale()來擷取到本地的語言locale對象,通過語言的判斷來傳回對應的語言locale對象,預設傳回簡體的locale對象,如果使用者選擇了英文則是傳回英文的Locale.ENGLISH,同理傳回其他對應語言的Locale對象。

/**
     * 如果不是英文、簡體中文、繁體中文,預設傳回簡體中文
     *
     * @return
     */
    private Locale getLanguageLocale() {
        int languageType = CommSharedUtil.getInstance(mContext).getInt(LanguageUtil.SAVE_LANGUAGE, );
        if (languageType == LanguageType.LANGUAGE_FOLLOW_SYSTEM) {
            Locale sysType = getSysLocale();
            if (sysType.equals(Locale.ENGLISH)) {
                return Locale.ENGLISH;
            } else if (sysType.equals(Locale.TRADITIONAL_CHINESE)) {
                return Locale.TRADITIONAL_CHINESE;
            } else if (TextUtils.equals(sysType.getLanguage(), Locale.CHINA.getLanguage())) { //zh
                if (TextUtils.equals(sysType.getCountry(), Locale.CHINA.getCountry())) {  //适配華為mate9  zh_CN_#Hans
                    return Locale.SIMPLIFIED_CHINESE;
                }
            } else {
                return Locale.SIMPLIFIED_CHINESE;
            }
        } else if (languageType == LanguageType.LANGUAGE_EN) {
            return Locale.ENGLISH;
        } else if (languageType == LanguageType.LANGUAGE_CHINESE_SIMPLIFIED) {
            return Locale.SIMPLIFIED_CHINESE;
        } else if (languageType == LanguageType.LANGUAGE_CHINESE_TRADITIONAL) {
            return Locale.TRADITIONAL_CHINESE;
        }
        Log.e(TAG, "getLanguageLocale" + languageType + languageType);
        getSystemLanguage(getSysLocale());
        return Locale.SIMPLIFIED_CHINESE;
    }
           

  在多語言切換選擇的時候,調用updateLanguage方法來實作切換到使用者選擇的語言類型

/**
     * 更新語言
     *
     * @param languageType
     */
    public void updateLanguage(int languageType) {
        CommSharedUtil.getInstance(mContext).putInt(LanguageUtil.SAVE_LANGUAGE, languageType);
        MultiLanguageUtil.getInstance().setConfiguration();
        EventBus.getDefault().post(new OnChangeLanguageEvent(languageType));
    }
           

  語言切換成功事件可以通過發生EventBus來通知想要知道這個事件的頁面,來實作語言變化後重新整理UI和更改請求接口中的語言參數重新請求服務端擷取對應語言的資料來填充布局。

  4. 多語言切換功能一般出現在二三級頁面,選擇其他語言确認之後跳轉到哪裡? Android 中很多的APP的做法是直接跳轉到首頁,比如微信都是直接删除所有的activity同時跳轉到首頁,這樣再次打開其他二級頁面的時候就會重新初始化頁面,重新加載語言當然也會跟着變化成使用者選擇的語言。當然傳回上一級頁面也是可以的,隻是處理起來會比較麻煩,是以推薦還是微信的切換語言之後的做法。

選擇完語言确認的跳轉邏輯如下:

@Override
    public void onClick(View view) {
        int id = view.getId();
        int selectedLanguage = ;
        switch (id) {
            case R.id.rl_followsytem:
                setFollowSytemVisible();
                selectedLanguage = LanguageType.LANGUAGE_FOLLOW_SYSTEM;
                break;
            case R.id.rl_simplified_chinese:
                setSimplifiedVisible();
                selectedLanguage = LanguageType.LANGUAGE_CHINESE_SIMPLIFIED;

                break;
            case R.id.rl_traditional_chinese:
                setTraditionalVisible();
                selectedLanguage = LanguageType.LANGUAGE_CHINESE_TRADITIONAL;

                break;
            case R.id.rl_english:
                setEnglishVisible();
                selectedLanguage = LanguageType.LANGUAGE_EN;
                break;
        }
        LanguageUtil.getInstance().updateLanguage(selectedLanguage);
        Intent intent = new Intent(SetLanguageActivity.this, MainActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        startActivity(intent);
        if (selectedLanguage == LanguageType.LANGUAGE_FOLLOW_SYSTEM) {
            System.exit();
        }
    }
           

  通過intent的 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK)來清除activity的棧,來打開MainActivity看起來像是重新開機的效果。這個跳轉過程要做到盡量的平滑,不然會被産品經理刁飛的。另外設定跟随系統有些特别,如果不先退出整個APP再回到首頁的話,則不會有切換語言的效果。

多語言開發過程中的遇到的問題

  1. 橫豎屏切換(螢幕旋轉)導緻多語言(國際化)的設定失效

  原因:當螢幕旋轉橫豎屏切換時,Activity的onConfigurationChanged方法使用了系統的語言設定,Activity中onConfigurationChanged的源碼如下:

@Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        getDelegate().onConfigurationChanged(newConfig);
        if (mResources != null) {
            // The real (and thus managed) resources object was already updated
            // by ResourcesManager, so pull the current metrics from there.
            final DisplayMetrics newMetrics = super.getResources().getDisplayMetrics();
            mResources.updateConfiguration(newConfig, newMetrics);
        }
    }
           

  從上面的代碼中我們可以看到newConfig取的是系統的,這就解釋了為什麼APP明明設定成了繁體,但是打開一個橫屏頁面之後,發現APP又重置成了簡體,就是這個方法導緻的。

  是以我們隻需要在onConfigurationChanged方法中,重新設定為我們使用者選擇的語言配置就可以了。因為要在所有的Activity中的onConfigurationChanged中設定會有些麻煩,我們可以在application中的onConfigurationChanged方法中統一設定,這樣就可以解決螢幕旋轉造成的多語言失效問題。

@Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        MultiLanguageUtil.getInstance().setConfiguration();
    }
           

  2. 打開有 webview 的Activity會導緻多語言失效,發生在Android7.0以上

  原因:可能是webview在加載的過程中也會重新設定系統的語言,這樣因為打開網站可能有英文版的和中文的。

  解決方案,既然很多情況下都會造成多語言失效,不如我們統一在BaseActivity的onCreate方法中都設定

當然如果有些Activity不是繼承子BaseActivity,我們依然可以在application中的onCreate方法中使用這樣的方式來拿到整個應用的所有activity的生命周期方法,然後在這樣設定:

registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle bundle) {

            }

            @Override
            public void onActivityStarted(Activity activity) {

            }

            @Override
            public void onActivityResumed(Activity activity) {
                MultiLanguageUtil.getInstance().setConfiguration();
            }

            @Override
            public void onActivityPaused(Activity activity) {

            }

            @Override
            public void onActivityStopped(Activity activity) {

            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {

            }

            @Override
            public void onActivityDestroyed(Activity activity) {

            }
        });
           

  3. 使用微信開源的熱修複架構Tinker,打了包含資源的更新檔之後會導緻多語言失效

  筆者在試用的過程中遇到如果打了包含資源string檔案的更新檔之後,會導緻多語言失效,本來選的繁體變成了簡體語言,同時無論你怎麼切換語言,都沒有生效。這屬于Tinker的bug,已經有人在Tinker的github首頁上回報了,但是這個issue 任然沒有關閉:https://github.com/Tencent/tinker/issues/302 。 後來因為擔心熱修複技術可能會存在其他相容性問題,是以還沒有用在公司的app上。但是“熱修複不是請客吃飯”,感謝Tinker開發者的努力。

  多語言(國際化)過程雖然繁瑣,充滿了重複勞動,但總體來說不算難。最後附示例項目位址,歡迎大家提問

https://github.com/finddreams/AndroidMultiLanguage

繼續閱讀