From 87d4779c2d4e26082a446cd6e7f3afeb771bf9ff Mon Sep 17 00:00:00 2001 From: Jonas Kalderstam Date: Mon, 2 Mar 2020 00:42:29 +0100 Subject: [PATCH 1/4] Fixed translations preferring secondary locale over english In the case where a non-standard region has been set for the primary system language, the secondary locale will be used for localized strings when available instead of the expected primary language. For example, set system locales to [en-SE, ja-JP], that is English with region Sweden, and Japanese with region Japan, most apps will display English descriptions but those which have a Japanese translation will display that instead. This commit adds a fallback case for when the primary locale has not matched any translations, but it's language part does. --- app/src/main/java/org/fdroid/fdroid/data/App.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index 386623b67..af7bb638e 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -534,6 +534,15 @@ public class App extends ValueObject implements Comparable, Parcelable { if (availableLocales.contains(languageTag)) { localesToUse.add(languageTag); } + if (localesToUse.isEmpty()) { + // In case of non-standard region like [en-SE] + for (String availableLocale : availableLocales) { + String availableLanguage = availableLocale.split("-")[0]; + if (languageTag.equals(availableLanguage)) { + localesToUse.add(availableLocale); + } + } + } if (Build.VERSION.SDK_INT >= 24) { LocaleList localeList = Resources.getSystem().getConfiguration().getLocales(); String[] sortedLocaleList = localeList.toLanguageTags().split(","); From 3406edefcd1807cc9352589ac86dbb725c3165b0 Mon Sep 17 00:00:00 2001 From: Marcus Hoffmann Date: Tue, 20 Oct 2020 21:46:07 +0200 Subject: [PATCH 2/4] tests for locale selection --- .../main/java/org/fdroid/fdroid/data/App.java | 8 +- .../java/org/fdroid/fdroid/TestUtils.java | 13 ++ .../fdroid/data/LocaleSelectionTest.java | 194 ++++++++++++++++++ 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index af7bb638e..25f1fcd2b 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -19,6 +19,7 @@ import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -544,7 +545,7 @@ public class App extends ValueObject implements Comparable, Parcelable { } } if (Build.VERSION.SDK_INT >= 24) { - LocaleList localeList = Resources.getSystem().getConfiguration().getLocales(); + LocaleList localeList = getLocales(); String[] sortedLocaleList = localeList.toLanguageTags().split(","); Arrays.sort(sortedLocaleList, new java.util.Comparator() { @Override @@ -624,6 +625,11 @@ public class App extends ValueObject implements Comparable, Parcelable { tvScreenshots = getLocalizedListEntry(localized, localesToUse, "tvScreenshots"); } + @RequiresApi(api = Build.VERSION_CODES.N) + LocaleList getLocales() { + return Resources.getSystem().getConfiguration().getLocales(); + } + /** * Returns the right localized version of this entry, based on an immitation of * the logic that Android/Java uses. On Android >= 24, this can get the diff --git a/app/src/test/java/org/fdroid/fdroid/TestUtils.java b/app/src/test/java/org/fdroid/fdroid/TestUtils.java index 3c860831c..ef5a46804 100644 --- a/app/src/test/java/org/fdroid/fdroid/TestUtils.java +++ b/app/src/test/java/org/fdroid/fdroid/TestUtils.java @@ -26,6 +26,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.security.NoSuchAlgorithmException; import static org.junit.Assert.assertEquals; @@ -177,4 +179,15 @@ public class TestUtils { AppProvider.Helper.calcSuggestedApks(context); AppProvider.Helper.recalculatePreferredMetadata(context); } + + /** + * Set a static final field through reflection + */ + public static void setFinalStatic(Field field, Object newValue) throws Exception { + field.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, newValue); + } } diff --git a/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java b/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java new file mode 100644 index 000000000..efc26e39c --- /dev/null +++ b/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java @@ -0,0 +1,194 @@ +package org.fdroid.fdroid.data; + + +import android.os.Build; +import android.os.LocaleList; +import org.fdroid.fdroid.TestUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class LocaleSelectionTest { + + + @Test + public void correctLocaleSelectionBeforeSDK24() throws Exception { + TestUtils.setFinalStatic(Build.VERSION.class.getDeclaredField("SDK_INT"), 19); + assertTrue(Build.VERSION.SDK_INT < 24); + App app; + + Map> localized = new HashMap<>(); + HashMap en_US = new HashMap<>(); + en_US.put("summary", "summary-en_US"); + HashMap de_AT = new HashMap<>(); + de_AT.put("summary", "summary-de_AT"); + HashMap de_DE = new HashMap<>(); + de_DE.put("summary", "summary-de_DE"); + HashMap sv = new HashMap<>(); + sv.put("summary", "summary-sv"); + HashMap sv_FI = new HashMap<>(); + sv_FI.put("summary", "summary-sv_FI"); + + localized.put("de-AT", de_AT); + localized.put("de-DE", de_DE); + localized.put("en-US", en_US); + localized.put("sv", sv); + localized.put("sv-FI", sv_FI); + + // Easy mode. en-US metadata with an en-US locale + Locale.setDefault(new Locale("en", "US")); + app = new App(); + app.setLocalized(localized); + assertEquals("summary-en_US", app.summary); + + // Fall back to en-US locale, when we have a different en locale + Locale.setDefault(new Locale("en", "UK")); + app = new App(); + app.setLocalized(localized); + assertEquals("summary-en_US", app.summary); + + // Fall back to language only + Locale.setDefault(new Locale("en", "UK")); + app = new App(); + app.setLocalized(localized); + assertEquals("summary-en_US", app.summary); + + // select the correct one out of multiple language locales + Locale.setDefault(new Locale("de", "DE")); + app = new App(); + app.setLocalized(localized); + assertEquals("summary-de_DE", app.summary); + + // Even when we have a non-exact matching locale, we should fall back to the same language + Locale.setDefault(new Locale("de", "CH")); + app = new App(); + app.setLocalized(localized); + assertEquals("summary-de_AT", app.summary); + + // Test fallback to base lang with not exact matching locale + Locale.setDefault(new Locale("sv", "SE")); + app = new App(); + app.setLocalized(localized); + assertEquals("summary-sv", app.summary); + } + + @Test + public void correctLocaleSelectionFromSDK24() throws Exception { + + TestUtils.setFinalStatic(Build.VERSION.class.getDeclaredField("SDK_INT"), 29); + assertTrue(Build.VERSION.SDK_INT >= 24); + + App app = spy(new App()); + LocaleList localeList = mock(LocaleList.class); + + // we mock both the getLocales call and the conversion to a language tag string. + doReturn(localeList).when(app).getLocales(); + // Set both default locale as well as the locale list, because the algorithm uses both... + Locale.setDefault(new Locale("en", "US")); + when(localeList.toLanguageTags()).thenReturn("en-US,de-DE"); + + //no metadata present + Map> localized = new HashMap<>(); + app.setLocalized(localized); + assertEquals("Unknown application", app.summary); + + HashMap en_US = new HashMap<>(); + en_US.put("summary", "summary-en_US"); + HashMap en_GB = new HashMap<>(); + en_GB.put("summary", "summary-en_GB"); + HashMap de_AT = new HashMap<>(); + de_AT.put("summary", "summary-de_AT"); + HashMap de_DE = new HashMap<>(); + de_DE.put("summary", "summary-de_DE"); + + app.summary = "reset"; + localized.put("de-AT", de_AT); + localized.put("de-DE", de_DE); + localized.put("en-US", en_US); + app.setLocalized(localized); + // just select the matching en-US locale, nothing special here + assertEquals("summary-en_US", app.summary); + + Locale.setDefault(new Locale("en", "SE")); + when(localeList.toLanguageTags()).thenReturn("en-SE,de-DE"); + app.setLocalized(localized); + // Fall back to another en locale before de + assertEquals("summary-en_US", app.summary); + + app.summary = "reset"; + localized.clear(); + localized.put("de-AT", de_AT); + localized.put("de-DE", de_DE); + localized.put("en-GB", en_GB); + localized.put("en-US", en_US); + + Locale.setDefault(new Locale("de", "AT")); + when(localeList.toLanguageTags()).thenReturn("de-AT,de-DE"); + app.setLocalized(localized); + // full match against a non-default locale + assertEquals("summary-de_AT", app.summary); + + app.summary = "reset"; + localized.clear(); + localized.put("de-AT", de_AT); + localized.put("de", de_DE); + localized.put("en-GB", en_GB); + localized.put("en-US", en_US); + + Locale.setDefault(new Locale("de", "CH")); + when(localeList.toLanguageTags()).thenReturn("de-CH,en-US"); + app.setLocalized(localized); + assertEquals("summary-de_DE", app.summary); + + app.summary = "reset"; + localized.clear(); + localized.put("en-GB", en_GB); + localized.put("en-US", en_US); + + Locale.setDefault(new Locale("en", "AU")); + when(localeList.toLanguageTags()).thenReturn("en-AU"); + app.setLocalized(localized); + assertEquals("summary-en_US", app.summary); + + app.summary = "reset"; + Locale.setDefault(new Locale("zh", "TW", "#Hant")); + when(localeList.toLanguageTags()).thenReturn("zh-Hant-TW,zh-Hans-CN"); + localized.clear(); + localized.put("en", en_GB); + localized.put("en-US", en_US); + app.setLocalized(localized); + //No match at all, fall back to an english locale + assertEquals("summary-en_US", app.summary); + + app.summary = "reset"; + HashMap zh_TW = new HashMap<>(); + zh_TW.put("summary", "summary-zh_TW"); + HashMap zh_CN = new HashMap<>(); + zh_CN.put("summary", "summary-zh_CN"); + + localized.clear(); + localized.put("en-US", en_US); + localized.put("zh-CN", zh_CN); + localized.put("zh-TW", zh_TW); + app.setLocalized(localized); + assertEquals("summary-zh_TW", app.summary); + + localized.clear(); + localized.put("en-US", en_US); + localized.put("zh-CN", zh_CN); + app.setLocalized(localized); + assertEquals("summary-zh_CN", app.summary); + } +} From 138b78572c627b6cec8dce974aa0114de2f2c764 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 20 Oct 2020 22:16:46 +0200 Subject: [PATCH 3/4] LocaleSelectionTest: reuse variables to make test cases clear --- .../fdroid/data/LocaleSelectionTest.java | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java b/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java index efc26e39c..52bef0a85 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) public class LocaleSelectionTest { + private static final String KEY = "summary"; @Test public void correctLocaleSelectionBeforeSDK24() throws Exception { @@ -31,15 +32,15 @@ public class LocaleSelectionTest { Map> localized = new HashMap<>(); HashMap en_US = new HashMap<>(); - en_US.put("summary", "summary-en_US"); + en_US.put(KEY, "summary-en_US"); HashMap de_AT = new HashMap<>(); - de_AT.put("summary", "summary-de_AT"); + de_AT.put(KEY, "summary-de_AT"); HashMap de_DE = new HashMap<>(); - de_DE.put("summary", "summary-de_DE"); + de_DE.put(KEY, "summary-de_DE"); HashMap sv = new HashMap<>(); - sv.put("summary", "summary-sv"); + sv.put(KEY, "summary-sv"); HashMap sv_FI = new HashMap<>(); - sv_FI.put("summary", "summary-sv_FI"); + sv_FI.put(KEY, "summary-sv_FI"); localized.put("de-AT", de_AT); localized.put("de-DE", de_DE); @@ -51,37 +52,37 @@ public class LocaleSelectionTest { Locale.setDefault(new Locale("en", "US")); app = new App(); app.setLocalized(localized); - assertEquals("summary-en_US", app.summary); + assertEquals(en_US.get(KEY), app.summary); // Fall back to en-US locale, when we have a different en locale Locale.setDefault(new Locale("en", "UK")); app = new App(); app.setLocalized(localized); - assertEquals("summary-en_US", app.summary); + assertEquals(en_US.get(KEY), app.summary); // Fall back to language only Locale.setDefault(new Locale("en", "UK")); app = new App(); app.setLocalized(localized); - assertEquals("summary-en_US", app.summary); + assertEquals(en_US.get(KEY), app.summary); // select the correct one out of multiple language locales Locale.setDefault(new Locale("de", "DE")); app = new App(); app.setLocalized(localized); - assertEquals("summary-de_DE", app.summary); + assertEquals(de_DE.get(KEY), app.summary); // Even when we have a non-exact matching locale, we should fall back to the same language Locale.setDefault(new Locale("de", "CH")); app = new App(); app.setLocalized(localized); - assertEquals("summary-de_AT", app.summary); + assertEquals(de_AT.get(KEY), app.summary); // Test fallback to base lang with not exact matching locale Locale.setDefault(new Locale("sv", "SE")); app = new App(); app.setLocalized(localized); - assertEquals("summary-sv", app.summary); + assertEquals(sv.get(KEY), app.summary); } @Test @@ -105,13 +106,13 @@ public class LocaleSelectionTest { assertEquals("Unknown application", app.summary); HashMap en_US = new HashMap<>(); - en_US.put("summary", "summary-en_US"); + en_US.put(KEY, "summary-en_US"); HashMap en_GB = new HashMap<>(); - en_GB.put("summary", "summary-en_GB"); + en_GB.put(KEY, "summary-en_GB"); HashMap de_AT = new HashMap<>(); - de_AT.put("summary", "summary-de_AT"); + de_AT.put(KEY, "summary-de_AT"); HashMap de_DE = new HashMap<>(); - de_DE.put("summary", "summary-de_DE"); + de_DE.put(KEY, "summary-de_DE"); app.summary = "reset"; localized.put("de-AT", de_AT); @@ -119,13 +120,13 @@ public class LocaleSelectionTest { localized.put("en-US", en_US); app.setLocalized(localized); // just select the matching en-US locale, nothing special here - assertEquals("summary-en_US", app.summary); + assertEquals(en_US.get(KEY), app.summary); Locale.setDefault(new Locale("en", "SE")); when(localeList.toLanguageTags()).thenReturn("en-SE,de-DE"); app.setLocalized(localized); // Fall back to another en locale before de - assertEquals("summary-en_US", app.summary); + assertEquals(en_US.get(KEY), app.summary); app.summary = "reset"; localized.clear(); @@ -138,7 +139,7 @@ public class LocaleSelectionTest { when(localeList.toLanguageTags()).thenReturn("de-AT,de-DE"); app.setLocalized(localized); // full match against a non-default locale - assertEquals("summary-de_AT", app.summary); + assertEquals(de_AT.get(KEY), app.summary); app.summary = "reset"; localized.clear(); @@ -150,7 +151,7 @@ public class LocaleSelectionTest { Locale.setDefault(new Locale("de", "CH")); when(localeList.toLanguageTags()).thenReturn("de-CH,en-US"); app.setLocalized(localized); - assertEquals("summary-de_DE", app.summary); + assertEquals(de_DE.get(KEY), app.summary); app.summary = "reset"; localized.clear(); @@ -160,7 +161,7 @@ public class LocaleSelectionTest { Locale.setDefault(new Locale("en", "AU")); when(localeList.toLanguageTags()).thenReturn("en-AU"); app.setLocalized(localized); - assertEquals("summary-en_US", app.summary); + assertEquals(en_US.get(KEY), app.summary); app.summary = "reset"; Locale.setDefault(new Locale("zh", "TW", "#Hant")); @@ -170,25 +171,25 @@ public class LocaleSelectionTest { localized.put("en-US", en_US); app.setLocalized(localized); //No match at all, fall back to an english locale - assertEquals("summary-en_US", app.summary); + assertEquals(en_US.get(KEY), app.summary); app.summary = "reset"; HashMap zh_TW = new HashMap<>(); - zh_TW.put("summary", "summary-zh_TW"); + zh_TW.put(KEY, "summary-zh_TW"); HashMap zh_CN = new HashMap<>(); - zh_CN.put("summary", "summary-zh_CN"); + zh_CN.put(KEY, "summary-zh_CN"); localized.clear(); localized.put("en-US", en_US); localized.put("zh-CN", zh_CN); localized.put("zh-TW", zh_TW); app.setLocalized(localized); - assertEquals("summary-zh_TW", app.summary); + assertEquals(zh_TW.get(KEY), app.summary); localized.clear(); localized.put("en-US", en_US); localized.put("zh-CN", zh_CN); app.setLocalized(localized); - assertEquals("summary-zh_CN", app.summary); + assertEquals(zh_CN.get(KEY), app.summary); } } From 33fb22eae1381b99bce32b12501808c287d63cbf Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 20 Oct 2020 22:22:02 +0200 Subject: [PATCH 4/4] LocaleSelectionTest: include test for TW/HK issue closes #2087 --- .../test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java b/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java index 52bef0a85..60ec8b872 100644 --- a/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java +++ b/app/src/test/java/org/fdroid/fdroid/data/LocaleSelectionTest.java @@ -178,10 +178,13 @@ public class LocaleSelectionTest { zh_TW.put(KEY, "summary-zh_TW"); HashMap zh_CN = new HashMap<>(); zh_CN.put(KEY, "summary-zh_CN"); + HashMap zh_HK = new HashMap<>(); + zh_HK.put(KEY, "summary-zh_HK"); localized.clear(); localized.put("en-US", en_US); localized.put("zh-CN", zh_CN); + localized.put("zh-HK", zh_HK); localized.put("zh-TW", zh_TW); app.setLocalized(localized); assertEquals(zh_TW.get(KEY), app.summary);