Skip to content

Update the native UI language without restarting the app #34

Description

@1280103995

It is better to not restart the app when changing the language in the app.

I asked AI to refer to your code and update the native UI on Android without restarting the app, but I couldn't solve it on iOS. I found this library, which claims to support iOS to switch the in-app language without restarting the app.

I have verified it on the library @react-native-community/datetimepicker: 8.3.0, and it works fine.

Here is the code for Android:

public class LocalizeModule extends ReactContextBaseJavaModule {

    private ReactApplicationContext reactContext;
    private static final String TAG = "LocalizeModule";

    public LocalizeModule(ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
    }

    @Override
    public String getName() {
        return "LocalizeModule";
    }

    /**
     * Get IETF BCP 47 (language-COUNTRY "pl-PL")
     * if country is not available in locale, then use system defaults (even if it's not 100% correct, like "pl-US")
     **/
    private String getLanguageTag(String language) {
        Locale locale = Locale.forLanguageTag(language);
        if (!TextUtils.isEmpty(locale.getCountry())) {
            return locale.toLanguageTag();
        }
        return new Locale(locale.getLanguage(), Locale.getDefault().getCountry()).toLanguageTag();
    }
    
    @ReactMethod
    public void getLanguage(Promise promise) {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                String currentLocaleName;
                if (!AppCompatDelegate.getApplicationLocales().isEmpty()) {
                    currentLocaleName = AppCompatDelegate.getApplicationLocales().get(0).toLanguageTag();
                } else {
                    currentLocaleName = Locale.getDefault().toLanguageTag();
                }
                promise.resolve(currentLocaleName);
            } else {
                SharedPreferences prefs = getPreferences();
                String savedLanguageFrom = prefs.getString("languageFrom", null);
                
                if (savedLanguageFrom != null && savedLanguageFrom.equals(Locale.getDefault().getLanguage())) {
                    promise.resolve(prefs.getString("language", Locale.getDefault().toLanguageTag()));
                } else {
                    promise.resolve(Locale.getDefault().toLanguageTag());
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "Error getting language", e);
            promise.reject("ERROR", "Failed to get language: " + e.getMessage());
        }
    }

    /**
     * passed language can be in ISO 639-1 (language "pl")
     * or IETF BCP 47 (language-COUNTRY "pl-PL")
     *  If API version >= 33, use native per-app language feature
     *  else, fallback to SharedPreferences
     **/
    @ReactMethod
    public void changeLanguage(String language, Promise promise) {
        try {
            Log.d(TAG, "Setting language to: " + language);

            String languageTag = getLanguageTag(language);
            Locale locale = Locale.forLanguageTag(languageTag);

            SharedPreferences.Editor editor = getPreferences().edit();
            editor.putString("languageFrom", Locale.getDefault().getLanguage());
            editor.putString("language", languageTag);
            editor.apply();
            
            Locale.setDefault(locale);
            
            Activity currentActivity = getCurrentActivity();
            if (currentActivity != null) {
                currentActivity.runOnUiThread(() -> {
                    try {
                        Configuration config = new Configuration(currentActivity.getResources().getConfiguration());
                        
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                            LocaleList localeList = new LocaleList(locale);
                            config.setLocales(localeList);
                        } else {
                            config.locale = locale;
                        }
                        
                        currentActivity.getResources().updateConfiguration(config, currentActivity.getResources().getDisplayMetrics());
                        
                        Context appContext = reactContext.getApplicationContext();
                        appContext.getResources().updateConfiguration(config, appContext.getResources().getDisplayMetrics());
                        
                        View rootView = currentActivity.getWindow().getDecorView().findViewById(android.R.id.content);
                        if (rootView != null) {
                            rootView.invalidate();
                        }
                        
                        Log.d(TAG, "Activity resources updated");
                    } catch (Exception e) {
                        Log.e(TAG, "Error updating activity resources", e);
                    }
                });
            }
            
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                LocaleListCompat localeList = LocaleListCompat.forLanguageTags(languageTag);
                AppCompatDelegate.setApplicationLocales(localeList);
            }
            
            promise.resolve(true);
            
            Log.d(TAG, "Language set successfully to: " + language);
        } catch (Exception e) {
            Log.e(TAG, "Error setting language", e);
            promise.reject("ERROR", "Failed to set language: " + e.getMessage());
        }
    }

    /**
     * SharedPreferences (only used when API version is below 33)
     **/
    private SharedPreferences getPreferences() {
        return reactContext.getSharedPreferences(
            "LocalizationSettings", Context.MODE_PRIVATE
        );
    }
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions