mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-06-18 04:39:45 -04:00
totally overhaul choosing locales from app metadata based on LocaleList
This makes the selection logic heed the list of preferred locales from the user Settings. closes #987 closes #1186 refs #1440 #1882 #1730 !886
This commit is contained in:
@@ -6,6 +6,7 @@ import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
import org.fdroid.fdroid.TestUtils;
|
||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
|
||||
@@ -303,7 +304,7 @@ public class AppProviderTest extends FDroidProviderTest {
|
||||
localized.put("es", es);
|
||||
localized.put("fr", fr);
|
||||
|
||||
Locale.setDefault(new Locale("nl", "NL"));
|
||||
App.systemLocaleList = LocaleListCompat.forLanguageTags("nl-NL");
|
||||
app.setLocalized(localized);
|
||||
assertFalse(app.isLocalized);
|
||||
|
||||
@@ -324,7 +325,7 @@ public class AppProviderTest extends FDroidProviderTest {
|
||||
app.setLocalized(localized);
|
||||
assertFalse(app.isLocalized);
|
||||
|
||||
Locale.setDefault(new Locale("en", "US"));
|
||||
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-US");
|
||||
app = new App();
|
||||
localized.clear();
|
||||
localized.put("en-US", en);
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.LocaleList;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.fdroid.fdroid.TestUtils;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.shadows.ShadowLog;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
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)
|
||||
@SuppressWarnings("LocalVariableName")
|
||||
@@ -25,83 +29,25 @@ public class LocaleSelectionTest {
|
||||
|
||||
private static final String KEY = "summary";
|
||||
|
||||
@Test
|
||||
public void correctLocaleSelectionBeforeSDK24() throws Exception {
|
||||
TestUtils.setFinalStatic(Build.VERSION.class.getDeclaredField("SDK_INT"), 19);
|
||||
assertTrue(Build.VERSION.SDK_INT < 24);
|
||||
App app;
|
||||
private static final String EN_US_NAME = "Checkey: info on local apps\n";
|
||||
private static final String EN_US_FEATURE_GRAPHIC = "en-US/featureGraphic.png";
|
||||
private static final String EN_US_PHONE_SCREENSHOT = "en-US/phoneScreenshots/First.png";
|
||||
private static final String EN_US_SEVEN_INCH_SCREENSHOT = "en-US/sevenInchScreenshots/checkey-tablet.png";
|
||||
private static final String FR_FR_NAME = "Checkey : infos applis locales";
|
||||
private static final String FR_CA_FEATURE_GRAPHIC = "fr-CA/featureGraphic.png";
|
||||
private static final String FR_FR_FEATURE_GRAPHIC = "fr-FR/featureGraphic.png";
|
||||
private static final String FR_FR_SEVEN_INCH_SCREENSHOT = "fr-FR/sevenInchScreenshots/checkey-tablet.png";
|
||||
|
||||
Map<String, Map<String, Object>> localized = new HashMap<>();
|
||||
HashMap<String, Object> en_US = new HashMap<>();
|
||||
en_US.put(KEY, "summary-en_US");
|
||||
HashMap<String, Object> de_AT = new HashMap<>();
|
||||
de_AT.put(KEY, "summary-de_AT");
|
||||
HashMap<String, Object> de_DE = new HashMap<>();
|
||||
de_DE.put(KEY, "summary-de_DE");
|
||||
HashMap<String, Object> sv = new HashMap<>();
|
||||
sv.put(KEY, "summary-sv");
|
||||
HashMap<String, Object> sv_FI = new HashMap<>();
|
||||
sv_FI.put(KEY, "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(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(en_US.get(KEY), app.summary);
|
||||
|
||||
// Fall back to language only
|
||||
Locale.setDefault(new Locale("en", "UK"));
|
||||
app = new App();
|
||||
app.setLocalized(localized);
|
||||
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(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(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(sv.get(KEY), app.summary);
|
||||
@Before
|
||||
public final void setUp() {
|
||||
ShadowLog.stream = System.out;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void correctLocaleSelectionFromSDK24() throws Exception {
|
||||
public void localeSelection() throws Exception {
|
||||
|
||||
TestUtils.setFinalStatic(Build.VERSION.class.getDeclaredField("SDK_INT"), 29);
|
||||
assertTrue(Build.VERSION.SDK_INT >= 24);
|
||||
|
||||
App app = spy(new App());
|
||||
|
||||
// we mock both the getLocales call and the conversion to a language tag string.
|
||||
Configuration configuration = mock(Configuration.class);
|
||||
LocaleList localeList = mock(LocaleList.class);
|
||||
doReturn(localeList).when(configuration).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");
|
||||
App app = new App();
|
||||
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-US,de-DE");
|
||||
|
||||
//no metadata present
|
||||
Map<String, Map<String, Object>> localized = new HashMap<>();
|
||||
@@ -112,10 +58,18 @@ public class LocaleSelectionTest {
|
||||
en_US.put(KEY, "summary-en_US");
|
||||
HashMap<String, Object> en_GB = new HashMap<>();
|
||||
en_GB.put(KEY, "summary-en_GB");
|
||||
HashMap<String, Object> de = new HashMap<>();
|
||||
de.put(KEY, "summary-de");
|
||||
HashMap<String, Object> de_AT = new HashMap<>();
|
||||
de_AT.put(KEY, "summary-de_AT");
|
||||
HashMap<String, Object> de_DE = new HashMap<>();
|
||||
de_DE.put(KEY, "summary-de_DE");
|
||||
HashMap<String, Object> es_ES = new HashMap<>();
|
||||
es_ES.put(KEY, "summary-es_ES");
|
||||
HashMap<String, Object> fr_FR = new HashMap<>();
|
||||
fr_FR.put(KEY, "summary-fr_FR");
|
||||
HashMap<String, Object> it_IT = new HashMap<>();
|
||||
it_IT.put(KEY, "summary-it_IT");
|
||||
|
||||
app.summary = "reset";
|
||||
localized.put("de-AT", de_AT);
|
||||
@@ -125,8 +79,7 @@ public class LocaleSelectionTest {
|
||||
// just select the matching en-US locale, nothing special here
|
||||
assertEquals(en_US.get(KEY), app.summary);
|
||||
|
||||
Locale.setDefault(new Locale("en", "SE"));
|
||||
when(localeList.toLanguageTags()).thenReturn("en-SE,de-DE");
|
||||
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-SE,de-DE");
|
||||
app.setLocalized(localized);
|
||||
// Fall back to another en locale before de
|
||||
assertEquals(en_US.get(KEY), app.summary);
|
||||
@@ -138,8 +91,7 @@ public class LocaleSelectionTest {
|
||||
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.systemLocaleList = LocaleListCompat.forLanguageTags("de-AT,de-DE");
|
||||
app.setLocalized(localized);
|
||||
// full match against a non-default locale
|
||||
assertEquals(de_AT.get(KEY), app.summary);
|
||||
@@ -147,14 +99,13 @@ public class LocaleSelectionTest {
|
||||
app.summary = "reset";
|
||||
localized.clear();
|
||||
localized.put("de-AT", de_AT);
|
||||
localized.put("de", de_DE);
|
||||
localized.put("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.systemLocaleList = LocaleListCompat.forLanguageTags("de-CH,en-US");
|
||||
app.setLocalized(localized);
|
||||
assertEquals(de_DE.get(KEY), app.summary);
|
||||
assertEquals(de.get(KEY), app.summary);
|
||||
|
||||
app.summary = "reset";
|
||||
localized.clear();
|
||||
@@ -162,13 +113,12 @@ public class LocaleSelectionTest {
|
||||
localized.put("en-US", en_US);
|
||||
|
||||
Locale.setDefault(new Locale("en", "AU"));
|
||||
when(localeList.toLanguageTags()).thenReturn("en-AU");
|
||||
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-AU");
|
||||
app.setLocalized(localized);
|
||||
assertEquals(en_US.get(KEY), app.summary);
|
||||
|
||||
app.summary = "reset";
|
||||
Locale.setDefault(new Locale("zh", "TW", "#Hant"));
|
||||
when(localeList.toLanguageTags()).thenReturn("zh-Hant-TW,zh-Hans-CN");
|
||||
App.systemLocaleList = LocaleListCompat.forLanguageTags("zh-Hant-TW,zh-Hans-CN");
|
||||
localized.clear();
|
||||
localized.put("en", en_GB);
|
||||
localized.put("en-US", en_US);
|
||||
@@ -197,5 +147,107 @@ public class LocaleSelectionTest {
|
||||
localized.put("zh-CN", zh_CN);
|
||||
app.setLocalized(localized);
|
||||
assertEquals(zh_CN.get(KEY), app.summary);
|
||||
|
||||
localized.clear();
|
||||
localized.put("en-US", en_US);
|
||||
localized.put("zh-CN", zh_CN);
|
||||
app.setLocalized(localized);
|
||||
assertEquals(zh_CN.get(KEY), app.summary);
|
||||
|
||||
// https://developer.android.com/guide/topics/resources/multilingual-support#resource-resolution-examples
|
||||
App.systemLocaleList = LocaleListCompat.forLanguageTags("fr-CH");
|
||||
localized.clear();
|
||||
localized.put("en-US", en_US);
|
||||
localized.put("de-DE", de_DE);
|
||||
localized.put("es-ES", es_ES);
|
||||
localized.put("fr-FR", fr_FR);
|
||||
localized.put("it-IT", it_IT);
|
||||
app.setLocalized(localized);
|
||||
assertEquals(fr_FR.get(KEY), app.summary);
|
||||
|
||||
// https://developer.android.com/guide/topics/resources/multilingual-support#t-2d-choice
|
||||
App.systemLocaleList = LocaleListCompat.forLanguageTags("fr-CH,it-CH");
|
||||
localized.clear();
|
||||
localized.put("en-US", en_US);
|
||||
localized.put("de-DE", de_DE);
|
||||
localized.put("es-ES", es_ES);
|
||||
localized.put("it-IT", it_IT);
|
||||
app.setLocalized(localized);
|
||||
assertEquals(it_IT.get(KEY), app.summary);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetLocalized() throws IOException {
|
||||
Assume.assumeTrue(Build.VERSION.SDK_INT >= 24);
|
||||
|
||||
File f = TestUtils.copyResourceToTempFile("localized.json");
|
||||
Map<String, Object> result = new ObjectMapper().readValue(
|
||||
FileUtils.readFileToString(f, (String) null), HashMap.class);
|
||||
List<Map<String, Object>> apps = (List<Map<String, Object>>) result.get("apps");
|
||||
Map<String, Map<String, Object>> localized = (Map<String, Map<String, Object>>) apps.get(0).get("localized");
|
||||
App app = new App();
|
||||
|
||||
App.systemLocaleList = LocaleListCompat.create(Locale.US);
|
||||
app.setLocalized(localized);
|
||||
assertEquals(EN_US_NAME, app.name);
|
||||
assertEquals(EN_US_FEATURE_GRAPHIC, app.featureGraphic);
|
||||
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
|
||||
assertEquals(EN_US_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
|
||||
assertTrue(app.isLocalized);
|
||||
|
||||
// choose the language when there is an exact locale match
|
||||
App.systemLocaleList = LocaleListCompat.forLanguageTags("fr-FR");
|
||||
app.setLocalized(localized);
|
||||
assertEquals(FR_FR_NAME, app.name);
|
||||
assertEquals(FR_FR_FEATURE_GRAPHIC, app.featureGraphic);
|
||||
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
|
||||
assertEquals(FR_FR_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
|
||||
assertTrue(app.isLocalized);
|
||||
|
||||
// choose the language from a different country when the preferred country is not available,
|
||||
// while still choosing featureGraphic from exact match
|
||||
App.systemLocaleList = LocaleListCompat.create(Locale.CANADA_FRENCH);
|
||||
app.setLocalized(localized);
|
||||
assertEquals(FR_FR_NAME, app.name);
|
||||
assertEquals(FR_CA_FEATURE_GRAPHIC, app.featureGraphic);
|
||||
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
|
||||
assertEquals(FR_FR_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
|
||||
assertTrue(app.isLocalized);
|
||||
|
||||
// choose the third preferred language when first and second lack translations
|
||||
App.systemLocaleList = LocaleListCompat.forLanguageTags("bo-IN,sr-RS,fr-FR");
|
||||
app.setLocalized(localized);
|
||||
assertEquals(FR_FR_NAME, app.name);
|
||||
assertEquals(FR_FR_FEATURE_GRAPHIC, app.featureGraphic);
|
||||
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
|
||||
assertEquals(FR_FR_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
|
||||
assertTrue(app.isLocalized);
|
||||
|
||||
// choose first language from different country, rather than 2nd full lang/country match
|
||||
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-GB,fr-FR");
|
||||
app.setLocalized(localized);
|
||||
assertEquals(EN_US_NAME, app.name);
|
||||
assertEquals(EN_US_FEATURE_GRAPHIC, app.featureGraphic);
|
||||
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
|
||||
assertEquals(EN_US_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
|
||||
assertTrue(app.isLocalized);
|
||||
|
||||
// choose en_US when no match, and mark as not localized
|
||||
App.systemLocaleList = LocaleListCompat.forLanguageTags("bo-IN,sr-RS");
|
||||
app.setLocalized(localized);
|
||||
assertEquals(EN_US_NAME, app.name);
|
||||
assertEquals(EN_US_FEATURE_GRAPHIC, app.featureGraphic);
|
||||
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
|
||||
assertEquals(EN_US_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
|
||||
assertFalse(app.isLocalized);
|
||||
|
||||
// When English is the preferred language and the second language has no entries
|
||||
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-US,sr-RS");
|
||||
app.setLocalized(localized);
|
||||
assertEquals(EN_US_NAME, app.name);
|
||||
assertEquals(EN_US_FEATURE_GRAPHIC, app.featureGraphic);
|
||||
assertEquals(EN_US_PHONE_SCREENSHOT, app.phoneScreenshots[0]);
|
||||
assertEquals(EN_US_SEVEN_INCH_SCREENSHOT, app.sevenInchScreenshots[0]);
|
||||
assertTrue(app.isLocalized);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,6 +370,7 @@ public class IndexV1UpdaterTest extends FDroidProviderTest {
|
||||
"isLocalized",
|
||||
"preferredSigner",
|
||||
"prefs",
|
||||
"systemLocaleList",
|
||||
"TAG",
|
||||
};
|
||||
runJsonIgnoreTest(new App(), allowedInApp, ignoredInApp);
|
||||
|
||||
Reference in New Issue
Block a user