diff options
| author | kwadronaut <kwadronaut@autistici.org> | 2026-06-26 15:32:17 +0000 |
|---|---|---|
| committer | cyberta <cyberta@riseup.net> | 2026-06-26 15:32:17 +0000 |
| commit | 8726545d77ffbdba0fccacbd6069e8690f269ea6 (patch) | |
| tree | 7d3fea44285cbc80b1a8dc89fa034e69f97e7e0d /app/src/main/java/se | |
| parent | b195a81641deb6d48675d8744ed863d2daf7469c (diff) | |
Builtin localized language switcher
Diffstat (limited to 'app/src/main/java/se')
4 files changed, 156 insertions, 32 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java index 5cd6c2a0..78407a12 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 LEAP Encryption Access Project and contributors + * Copyright (c) 2021-2026 LEAP Encryption Access Project and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java index 263a8d46..8b6efbc7 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java @@ -1,7 +1,12 @@ package se.leap.bitmaskclient.base.fragments; +import static android.content.res.Resources.getSystem; +import static androidx.core.app.LocaleManagerCompat.getSystemLocales; +import static androidx.core.os.ConfigurationCompat.getLocales; import static se.leap.bitmaskclient.base.utils.ViewHelper.setActionBarSubtitle; +import android.content.Context; +import android.content.res.Configuration; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -17,19 +22,19 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Locale; import se.leap.bitmaskclient.R; +import se.leap.bitmaskclient.base.utils.StringUtils; import se.leap.bitmaskclient.base.views.SimpleCheckBox; import se.leap.bitmaskclient.databinding.FLanguageSelectionBinding; import se.leap.bitmaskclient.databinding.VSelectTextListItemBinding; public class LanguageSelectionFragment extends BottomSheetDialogFragment { - static final String TAG = LanguageSelectionFragment.class.getSimpleName(); + public static final String TAG = LanguageSelectionFragment.class.getSimpleName(); static final String SYSTEM_LOCALE = "systemLocale"; private FLanguageSelectionBinding binding; @@ -63,29 +68,35 @@ public class LanguageSelectionFragment extends BottomSheetDialogFragment { } private void initRecyclerView(List<String> supportedLanguages) { - Locale defaultLocale = AppCompatDelegate.getApplicationLocales().get(0); - if (defaultLocale == null) { - defaultLocale = LocaleListCompat.getDefault().get(0); - } - // NOTE: Sort the supported languages by their display names. - // This would make updating supported languages easier as we don't have to tip toe around the order - Collections.sort(supportedLanguages, (lang1, lang2) -> { - String displayName1 = Locale.forLanguageTag(lang1).getDisplayName(Locale.ENGLISH); - String displayName2 = Locale.forLanguageTag(lang2).getDisplayName(Locale.ENGLISH); - return displayName1.compareTo(displayName2); + Locale currentLocale = getCurrentLocale(); + + // new list so it doesn't crash on fixed size + List<String> languagesWithSystem = new ArrayList<>(supportedLanguages); + + // Sort by the language's native name (endonym) + languagesWithSystem.sort((lang1, lang2) -> { + Locale loc1 = Locale.forLanguageTag(lang1); + Locale loc2 = Locale.forLanguageTag(lang2); + + String name1 = loc1.getDisplayName(loc1); + String name2 = loc2.getDisplayName(loc2); + + return name1.compareToIgnoreCase(name2); }); + + // using add(0) because addFirst() causes a NoSuchMethodError on Android 7 + languagesWithSystem.add(0, ""); + binding.languages.setAdapter( - new LanguageSelectionAdapter(supportedLanguages, this::updateLocale, defaultLocale) + new LanguageSelectionAdapter(languagesWithSystem, this::updateLocale, currentLocale) ); binding.languages.setLayoutManager(new LinearLayoutManager(getContext())); } + // use current locale or fallback to system default public static Locale getCurrentLocale() { - Locale defaultLocale = AppCompatDelegate.getApplicationLocales().get(0); - if (defaultLocale == null) { - defaultLocale = LocaleListCompat.getDefault().get(0); - } - return defaultLocale; + LocaleListCompat appLocales = AppCompatDelegate.getApplicationLocales(); + return appLocales.isEmpty() ? LocaleListCompat.getDefault().get(0) : appLocales.get(0); } /** @@ -102,7 +113,7 @@ public class LanguageSelectionFragment extends BottomSheetDialogFragment { } /** - * Adapter for the language selection recycler view. + * Adapter for language selection recycler view. */ static class LanguageSelectionAdapter extends RecyclerView.Adapter<LanguageSelectionAdapter.LanguageViewHolder> { @@ -126,12 +137,62 @@ public class LanguageSelectionFragment extends BottomSheetDialogFragment { @Override public void onBindViewHolder(@NonNull LanguageViewHolder holder, int position) { String languageTag = languages.get(position); - holder.languageName.setText(Locale.forLanguageTag(languageTag).getDisplayName(Locale.ENGLISH)); - if (languages.contains(selectedLocale.toLanguageTag())) { - holder.selected.setChecked(selectedLocale.toLanguageTag().equals(languageTag)); + + // using system lang or not + boolean isSystemSet = AppCompatDelegate.getApplicationLocales().isEmpty(); + + if (languageTag.isEmpty()) { + // handle empty string (system default) + Locale systemLocale = null; + + try { + // Use LocaleManagerCompat to reliably bypass app-level locale overrides + LocaleListCompat trueSystemLocales = getSystemLocales(holder.itemView.getContext()); + if (!trueSystemLocales.isEmpty()) { + systemLocale = trueSystemLocales.get(0); + } + } catch (Exception e) { + // Fallback for older AndroidX library versions + } + + if (systemLocale == null) { + systemLocale = getLocales(getSystem().getConfiguration()).get(0); + } + + String systemLabel = systemLocale != null ? + getLocalizedSystemLabel(holder.itemView.getContext(), systemLocale) : + ""; + + holder.languageName.setText(systemLabel); + holder.selected.setChecked(isSystemSet); + } else { - holder.selected.setChecked(selectedLocale.getLanguage().equals(languageTag)); + // standard language + Locale targetLocale = Locale.forLanguageTag(languageTag); + + // Pass targetLocale into getDisplayName to get the native name (endonym) + String displayName = targetLocale.getDisplayName(targetLocale); + + // Capitalize the first letter + if (!displayName.isEmpty()) { + displayName = StringUtils.capitalize(displayName, targetLocale); + } + + holder.languageName.setText(displayName); + + // no double-check items if system default is active + if (isSystemSet) { + holder.selected.setChecked(false); + } else { + // match exact tag or just the language code + if (languages.contains(selectedLocale.toLanguageTag())) { + holder.selected.setChecked(selectedLocale.toLanguageTag().equals(languageTag)); + } else { + holder.selected.setChecked(selectedLocale.getLanguage().equals(languageTag)); + } + } } + holder.itemView.setOnClickListener(v -> clickListener.onLanguageClick(languageTag)); } @@ -140,6 +201,22 @@ public class LanguageSelectionFragment extends BottomSheetDialogFragment { return languages.size(); } + // Language name in system language, suffix translated to current app language + private String getLocalizedSystemLabel(Context context, Locale systemLocale) { + // 1. Force the suffix to fetch strings in the APP'S current language (selectedLocale) + Configuration appConfig = new Configuration(context.getResources().getConfiguration()); + appConfig.setLocale(selectedLocale); + Context appLocalizedContext = context.createConfigurationContext(appConfig); + + String suffix = appLocalizedContext.getString(R.string.system_default); + + // 2. The language name uses its endonym (true system language's own name) + String languageName = systemLocale.getDisplayName(systemLocale); + languageName = StringUtils.capitalize(languageName, systemLocale); + + return String.format("%s\n(%s)", languageName, suffix); + } + /** * View holder for the language item */ @@ -156,11 +233,10 @@ public class LanguageSelectionFragment extends BottomSheetDialogFragment { } } - /** * Interface for the language click listener */ interface LanguageClickListener { void onLanguageClick(String languageTag); } -} +}
\ No newline at end of file diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java index a16d5565..98d0fb8c 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 LEAP Encryption Access Project and contributers + * Copyright (c) 2026 LEAP Encryption Access Project and contributers * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,8 +17,11 @@ package se.leap.bitmaskclient.base.fragments; +import static android.content.res.Resources.getSystem; import static android.view.View.GONE; import static android.view.View.VISIBLE; +import static androidx.core.os.ConfigurationCompat.getLocales; +import static se.leap.bitmaskclient.base.fragments.LanguageSelectionFragment.getCurrentLocale; import static se.leap.bitmaskclient.base.models.Constants.DONATION_URL; import static se.leap.bitmaskclient.base.models.Constants.ENABLE_DONATION; import static se.leap.bitmaskclient.base.models.Constants.PREFERRED_CITY; @@ -46,6 +49,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.graphics.drawable.DrawerArrowDrawable; import androidx.appcompat.widget.Toolbar; import androidx.core.content.ContextCompat; @@ -64,6 +68,7 @@ import se.leap.bitmaskclient.base.MainActivity; import se.leap.bitmaskclient.base.models.Provider; import se.leap.bitmaskclient.base.models.ProviderObservable; import se.leap.bitmaskclient.base.utils.PreferenceHelper; +import se.leap.bitmaskclient.base.utils.StringUtils; import se.leap.bitmaskclient.base.utils.ViewHelper; import se.leap.bitmaskclient.base.views.IconSwitchEntry; import se.leap.bitmaskclient.base.views.IconTextEntry; @@ -319,12 +324,29 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen }); } - private void initLanguageSettingsEntry() { IconTextEntry languageSwitcher = drawerView.findViewById(R.id.language_switcher); - Locale currentLocale = LanguageSelectionFragment.getCurrentLocale(); - languageSwitcher.setSubtitle(currentLocale.getDisplayName(Locale.ENGLISH)); + if (AppCompatDelegate.getApplicationLocales().isEmpty()) { + // system locale + Locale systemLocale = getLocales(getSystem().getConfiguration()).get(0); + // getDisplayLanguage() gets "English" instead of "English (United States)" + String languageName = systemLocale != null ? + systemLocale.getDisplayLanguage(systemLocale) : + ""; + languageName = StringUtils.capitalize(languageName, systemLocale); + + // Don't add "System default" suffix here, no multi line possible + languageSwitcher.setSubtitle(languageName); + } else { + // Standard user-selected language + Locale currentLocale = getCurrentLocale(); + + // Use getDisplayLanguage() for consistency + String languageName = currentLocale.getDisplayLanguage(currentLocale); + languageName = StringUtils.capitalize(languageName,currentLocale); + languageSwitcher.setSubtitle(languageName); + } languageSwitcher.setOnClickListener(v -> { FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager()); @@ -347,7 +369,6 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen closeDrawer(); Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATION_URL)); startActivity(browserIntent); - }); } } diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/StringUtils.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/StringUtils.java new file mode 100644 index 00000000..a5edde41 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/StringUtils.java @@ -0,0 +1,27 @@ +package se.leap.bitmaskclient.base.utils; + +import androidx.annotation.Nullable; + +import java.util.Locale; + +public class StringUtils { + + /** + * Capitalizes the first character of the given string using the specified locale. + * + * <p>This method handles null and empty strings by returning them unchanged. + * If the locale is null, the string is also returned unchanged.</p> + * + * @param string the string to be capitalized, may be null or empty + * @param locale the locale to be used for capitalization, may be null + * @return the capitalized string, or the original string + * @see String#toUpperCase(Locale) + */ + public static @Nullable String capitalize(@Nullable String string, @Nullable Locale locale) { + if (string == null || string.isEmpty() || locale == null) { + return string; + } + + return string.substring(0, 1).toUpperCase(locale) + string.substring(1); + } +} |
