summaryrefslogtreecommitdiff
path: root/app/src/main/java/se
diff options
context:
space:
mode:
authorkwadronaut <kwadronaut@autistici.org>2026-06-26 15:32:17 +0000
committercyberta <cyberta@riseup.net>2026-06-26 15:32:17 +0000
commit8726545d77ffbdba0fccacbd6069e8690f269ea6 (patch)
tree7d3fea44285cbc80b1a8dc89fa034e69f97e7e0d /app/src/main/java/se
parentb195a81641deb6d48675d8744ed863d2daf7469c (diff)
Builtin localized language switcher
Diffstat (limited to 'app/src/main/java/se')
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java128
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java31
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/StringUtils.java27
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);
+ }
+}