Set Locale programmatically

后端 未结 14 1440
Happy的楠姐
Happy的楠姐 2020-11-22 05:55

My app supports 3 (soon 4) languages. Since several locales are quite similar I\'d like to give the user the option to change locale in my application, for instance an Itali

相关标签:
14条回答
  • 2020-11-22 06:32

    As no answer is complete for the current way to solve this problem, I try to give instructions for a complete solution. Please comment if something is missing or could be done better.

    General information

    First, there exist some libraries that want to solve the problem but they all seem outdated or are missing some features:

    • https://github.com/delight-im/Android-Languages
      • Outdated (see issues)
      • When selecting a language in the UI, it always shows all languages (hardcoded in the library), not only those where translations exist
    • https://github.com/akexorcist/Android-LocalizationActivity
      • Seems quite sophisticated and might use a similar approach as shown below

    Further I think writing a library might not be a good/easy way to solve this problem because there is not very much to do, and what has to be done is rather changing existing code than using something completely decoupled. Therefore I composed the following instructions that should be complete.

    My solution is mainly based on https://github.com/gunhansancar/ChangeLanguageExample (as already linked to by localhost). It is the best code I found to orientate at. Some remarks:

    • As necessary, it provides different implementations to change locale for Android N (and above) and below
    • It uses a method updateViews() in each Activity to manually update all strings after changing locale (using the usual getString(id)) which is not necessary in the approach shown below
    • It only supports languages and not complete locales (which also include region (country) and variant codes)

    I changed it a bit, decoupling the part which persists the chosen locale (as one might want to do that separately, as suggested below).

    Solution

    The solution consists of the following two steps:

    • Permanently change the locale to be used by the app
    • Make the app use the custom locale set, without restarting

    Step 1: Change the locale

    Use the class LocaleHelper, based on gunhansancar's LocaleHelper:

    • Add a ListPreference in a PreferenceFragment with the available languages (has to be maintained when languages should be added later)
    import android.annotation.TargetApi;
    import android.content.Context;
    import android.content.SharedPreferences;
    import android.content.res.Configuration;
    import android.content.res.Resources;
    import android.os.Build;
    import android.preference.PreferenceManager;
    
    import java.util.Locale;
    
    import mypackage.SettingsFragment;
    
    /**
     * Manages setting of the app's locale.
     */
    public class LocaleHelper {
    
        public static Context onAttach(Context context) {
            String locale = getPersistedLocale(context);
            return setLocale(context, locale);
        }
    
        public static String getPersistedLocale(Context context) {
            SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
            return preferences.getString(SettingsFragment.KEY_PREF_LANGUAGE, "");
        }
    
        /**
         * Set the app's locale to the one specified by the given String.
         *
         * @param context
         * @param localeSpec a locale specification as used for Android resources (NOTE: does not
         *                   support country and variant codes so far); the special string "system" sets
         *                   the locale to the locale specified in system settings
         * @return
         */
        public static Context setLocale(Context context, String localeSpec) {
            Locale locale;
            if (localeSpec.equals("system")) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    locale = Resources.getSystem().getConfiguration().getLocales().get(0);
                } else {
                    //noinspection deprecation
                    locale = Resources.getSystem().getConfiguration().locale;
                }
            } else {
                locale = new Locale(localeSpec);
            }
            Locale.setDefault(locale);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                return updateResources(context, locale);
            } else {
                return updateResourcesLegacy(context, locale);
            }
        }
    
        @TargetApi(Build.VERSION_CODES.N)
        private static Context updateResources(Context context, Locale locale) {
            Configuration configuration = context.getResources().getConfiguration();
            configuration.setLocale(locale);
            configuration.setLayoutDirection(locale);
    
            return context.createConfigurationContext(configuration);
        }
    
        @SuppressWarnings("deprecation")
        private static Context updateResourcesLegacy(Context context, Locale locale) {
            Resources resources = context.getResources();
    
            Configuration configuration = resources.getConfiguration();
            configuration.locale = locale;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                configuration.setLayoutDirection(locale);
            }
    
            resources.updateConfiguration(configuration, resources.getDisplayMetrics());
    
            return context;
        }
    }
    

    Create a SettingsFragment like the following:

    import android.content.SharedPreferences;
    import android.os.Bundle;
    import android.preference.PreferenceFragment;
    import android.preference.PreferenceManager;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    
    import mypackage.LocaleHelper;
    import mypackage.R;
    
    /**
     * Fragment containing the app's main settings.
     */
    public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
        public static final String KEY_PREF_LANGUAGE = "pref_key_language";
    
        public SettingsFragment() {
            // Required empty public constructor
        }
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            addPreferencesFromResource(R.xml.preferences);
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fragment_settings, container, false);
            return view;
        }
    
        @Override
        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
            switch (key) {
                case KEY_PREF_LANGUAGE:
                    LocaleHelper.setLocale(getContext(), PreferenceManager.getDefaultSharedPreferences(getContext()).getString(key, ""));
                    getActivity().recreate(); // necessary here because this Activity is currently running and thus a recreate() in onResume() would be too late
                    break;
            }
        }
    
        @Override
        public void onResume() {
            super.onResume();
            // documentation requires that a reference to the listener is kept as long as it may be called, which is the case as it can only be called from this Fragment
            getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
        }
    
        @Override
        public void onPause() {
            super.onPause();
            getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
        }
    }
    

    Create a resource locales.xml listing all locales with available translations in the following way (list of locale codes):

    <!-- Lists available locales used for setting the locale manually.
         For now only language codes (locale codes without country and variant) are supported.
         Has to be in sync with "settings_language_values" in strings.xml (the entries must correspond).
      -->
    <resources>
        <string name="system_locale" translatable="false">system</string>
        <string name="default_locale" translatable="false"></string>
        <string-array name="locales">
            <item>@string/system_locale</item> <!-- system setting -->
            <item>@string/default_locale</item> <!-- default locale -->
            <item>de</item>
        </string-array>
    </resources>
    

    In your PreferenceScreen you can use the following section to let the user select the available languages:

    <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
        <PreferenceCategory
            android:title="@string/preferences_category_general">
            <ListPreference
                android:key="pref_key_language"
                android:title="@string/preferences_language"
                android:dialogTitle="@string/preferences_language"
                android:entries="@array/settings_language_values"
                android:entryValues="@array/locales"
                android:defaultValue="@string/system_locale"
                android:summary="%s">
            </ListPreference>
        </PreferenceCategory>
    </PreferenceScreen>
    

    which uses the following strings from strings.xml:

    <string name="preferences_category_general">General</string>
    <string name="preferences_language">Language</string>
    <!-- NOTE: Has to correspond to array "locales" in locales.xml (elements in same orderwith) -->
    <string-array name="settings_language_values">
        <item>Default (System setting)</item>
        <item>English</item>
        <item>German</item>
    </string-array>
    

    Step 2: Make the app use the custom locale

    Now setup each Activity to use the custom locale set. The easiest way to accomplish this is to have a common base class for all activities with the following code (where the important code is in attachBaseContext(Context base) and onResume()):

    import android.content.Context;
    import android.content.Intent;
    import android.content.pm.PackageInfo;
    import android.content.pm.PackageManager;
    import android.os.Bundle;
    import android.support.annotation.Nullable;
    import android.support.v7.app.AppCompatActivity;
    import android.view.Menu;
    import android.view.MenuInflater;
    import android.view.MenuItem;
    
    import mypackage.LocaleHelper;
    import mypackage.R;
    
    /**
     * {@link AppCompatActivity} with main menu in the action bar. Automatically recreates
     * the activity when the locale has changed.
     */
    public class MenuAppCompatActivity extends AppCompatActivity {
        private String initialLocale;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            initialLocale = LocaleHelper.getPersistedLocale(this);
        }
    
        @Override
        public boolean onCreateOptionsMenu(Menu menu) {
            MenuInflater inflater = getMenuInflater();
            inflater.inflate(R.menu.menu, menu);
            return true;
        }
    
        @Override
        public boolean onOptionsItemSelected(MenuItem item) {
            switch (item.getItemId()) {
                case R.id.menu_settings:
                    Intent intent = new Intent(this, SettingsActivity.class);
                    startActivity(intent);
                    return true;
                default:
                    return super.onOptionsItemSelected(item);
            }
        }
    
        @Override
        protected void attachBaseContext(Context base) {
            super.attachBaseContext(LocaleHelper.onAttach(base));
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            if (initialLocale != null && !initialLocale.equals(LocaleHelper.getPersistedLocale(this))) {
                recreate();
            }
        }
    }
    

    What it does is

    • Override attachBaseContext(Context base) to use the locale previously persisted with LocaleHelper
    • Detect a change of the locale and recreate the Activity to update its strings

    Notes on this solution

    • Recreating an Activity does not update the title of the ActionBar (as already observed here: https://github.com/gunhansancar/ChangeLanguageExample/issues/1).

      • This can be achieved by simply having a setTitle(R.string.mytitle) in the onCreate() method of each activity.
    • It lets the user chose the system default locale, as well as the default locale of the app (which can be named, in this case "English").

    • Only language codes, no region (country) and variant codes (like fr-rCA) are supported so far. To support full locale specifications, a parser similar to that in the Android-Languages library can be used (which supports region but no variant codes).

      • If someone finds or has written a good parser, add a comment so I can include it in the solution.
    0 讨论(0)
  • 2020-11-22 06:35

    I found the androidx.appcompat:appcompat:1.1.0 bug can also be fixed by simply calling getResources() in applyOverrideConfiguration()

    @Override public void
    applyOverrideConfiguration(Configuration cfgOverride)
    {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
          Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
        // add this to fix androidx.appcompat:appcompat 1.1.0 bug
        // which happens on Android 6.x ~ 7.x
        getResources();
      }
    
      super.applyOverrideConfiguration(cfgOverride);
    }
    
    0 讨论(0)
提交回复
热议问题