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:
parent
fbbf78dcf8
commit
e35335d59c
@ -67,6 +67,7 @@ import org.apache.commons.net.util.SubnetUtils;
|
|||||||
import org.fdroid.fdroid.Preferences.ChangeListener;
|
import org.fdroid.fdroid.Preferences.ChangeListener;
|
||||||
import org.fdroid.fdroid.Preferences.Theme;
|
import org.fdroid.fdroid.Preferences.Theme;
|
||||||
import org.fdroid.fdroid.compat.PRNGFixes;
|
import org.fdroid.fdroid.compat.PRNGFixes;
|
||||||
|
import org.fdroid.fdroid.data.App;
|
||||||
import org.fdroid.fdroid.data.AppProvider;
|
import org.fdroid.fdroid.data.AppProvider;
|
||||||
import org.fdroid.fdroid.data.InstalledAppProviderService;
|
import org.fdroid.fdroid.data.InstalledAppProviderService;
|
||||||
import org.fdroid.fdroid.data.Repo;
|
import org.fdroid.fdroid.data.Repo;
|
||||||
@ -330,6 +331,7 @@ public class FDroidApp extends Application {
|
|||||||
public void onConfigurationChanged(Configuration newConfig) {
|
public void onConfigurationChanged(Configuration newConfig) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
Languages.setLanguage(this);
|
Languages.setLanguage(this);
|
||||||
|
App.systemLocaleList = null;
|
||||||
|
|
||||||
// update the descriptions based on the new language preferences
|
// update the descriptions based on the new language preferences
|
||||||
SharedPreferences atStartTime = getAtStartTimeSharedPreferences();
|
SharedPreferences atStartTime = getAtStartTimeSharedPreferences();
|
||||||
|
@ -10,7 +10,6 @@ import android.content.res.AssetManager;
|
|||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.content.res.XmlResourceParser;
|
import android.content.res.XmlResourceParser;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.os.LocaleList;
|
import android.os.LocaleList;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
@ -19,6 +18,8 @@ import android.text.TextUtils;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.os.ConfigurationCompat;
|
||||||
|
import androidx.core.os.LocaleListCompat;
|
||||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
@ -41,8 +42,8 @@ import java.util.Arrays;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -75,6 +76,14 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private static final String TAG = "App";
|
private static final String TAG = "App";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link LocaleListCompat} for finding the right app description material.
|
||||||
|
* It is set globally static to a) cache this value, since there are thousands
|
||||||
|
* of {@link App} entries, and b) make it easy to test {@link #setLocalized(Map)} )}
|
||||||
|
*/
|
||||||
|
@JsonIgnore
|
||||||
|
public static LocaleListCompat systemLocaleList;
|
||||||
|
|
||||||
// these properties are not from the index metadata, but represent the state on the device
|
// these properties are not from the index metadata, but represent the state on the device
|
||||||
/**
|
/**
|
||||||
* True if compatible with the device (i.e. if at least one apk is)
|
* True if compatible with the device (i.e. if at least one apk is)
|
||||||
@ -98,8 +107,12 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
|||||||
public String preferredSigner;
|
public String preferredSigner;
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public boolean isApk;
|
public boolean isApk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has this {@code App} been localized into one of the user's current locales.
|
||||||
|
*/
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
boolean isLocalized = false;
|
boolean isLocalized;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is primarily for the purpose of saving app metadata when parsing an index.xml file.
|
* This is primarily for the purpose of saving app metadata when parsing an index.xml file.
|
||||||
@ -484,110 +497,25 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
|||||||
* {@code localized} block is included in the index. Also, null strings in
|
* {@code localized} block is included in the index. Also, null strings in
|
||||||
* the {@code localized} block should not overwrite Name/Summary/Description
|
* the {@code localized} block should not overwrite Name/Summary/Description
|
||||||
* strings with empty/null if they were set directly by Jackson.
|
* strings with empty/null if they were set directly by Jackson.
|
||||||
* <p>
|
* <ol>
|
||||||
* Choosing the locale to use follows two sets of rules, one for Android versions
|
|
||||||
* older than {@code android-24} and the other for {@code android-24} or newer.
|
|
||||||
* The system-wide language preference list was added in {@code android-24}.
|
|
||||||
* <ul>
|
|
||||||
* <li>{@code >= android-24}<ol>
|
|
||||||
* <li>the country variant {@code de-AT} from the user locale list
|
* <li>the country variant {@code de-AT} from the user locale list
|
||||||
* <li>only the language {@code de} from the above locale
|
* <li>only the language {@code de} from the above locale
|
||||||
|
* <li>next locale in the user's preference list ({@code >= android-24})
|
||||||
* <li>{@code en-US} since its the most common English for software
|
* <li>{@code en-US} since its the most common English for software
|
||||||
* <li>the first available {@code en} locale
|
* <li>the first available {@code en} locale
|
||||||
* </ol></li>
|
* </ol>
|
||||||
* <li>{@code < android-24}<ol>
|
|
||||||
* <li>the country variant from the user locale: {@code de-AT}
|
|
||||||
* <li>only the language from the above locale: {@code de}
|
|
||||||
* <li>all available locales with the same language: {@code de-BE}
|
|
||||||
* <li>{@code en-US} since its the most common English for software
|
|
||||||
* <li>all available {@code en} locales
|
|
||||||
* </ol></li>
|
|
||||||
* </ul>
|
|
||||||
* On {@code >= android-24}, it is by design that this does not fallback to other
|
|
||||||
* country-specific locales, e.g. {@code fr-CH} does not fall back on {@code fr-FR}.
|
|
||||||
* If someone wants to fallback to {@code fr-FR}, they can add it to the system
|
|
||||||
* language list. There are many cases where it is inappropriate to fallback to a
|
|
||||||
* different country-specific locale, for example {@code de-DE --> de-CH} or
|
|
||||||
* {@code zh-CN --> zh-TW}.
|
|
||||||
* <p>
|
* <p>
|
||||||
* On {@code < android-24}, the user can only set a single
|
* The system-wide language preference list was added in {@code android-24}.
|
||||||
* locale with a country as an option, so here it makes sense to try to fallback
|
*
|
||||||
* on other country-specific locales, rather than English.
|
* @see <a href="https://developer.android.com/guide/topics/resources/multilingual-support">Android language and locale resolution overview</a>
|
||||||
*/
|
*/
|
||||||
@JsonProperty("localized")
|
@JsonProperty("localized")
|
||||||
void setLocalized(Map<String, Map<String, Object>> localized) { // NOPMD
|
void setLocalized(Map<String, Map<String, Object>> localized) { // NOPMD
|
||||||
Locale defaultLocale = Locale.getDefault();
|
if (systemLocaleList == null) {
|
||||||
String languageTag = defaultLocale.getLanguage();
|
systemLocaleList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration());
|
||||||
String countryTag = defaultLocale.getCountry();
|
|
||||||
String localeTag;
|
|
||||||
if (TextUtils.isEmpty(countryTag)) {
|
|
||||||
localeTag = languageTag;
|
|
||||||
} else {
|
|
||||||
localeTag = languageTag + "-" + countryTag;
|
|
||||||
}
|
}
|
||||||
|
Set<String> localesToUse = localized.keySet();
|
||||||
Set<String> availableLocales = localized.keySet();
|
setIsLocalized(localesToUse);
|
||||||
Set<String> localesToUse = new LinkedHashSet<>();
|
|
||||||
if (availableLocales.contains(localeTag)) {
|
|
||||||
localesToUse.add(localeTag);
|
|
||||||
}
|
|
||||||
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(",");
|
|
||||||
Arrays.sort(sortedLocaleList, new java.util.Comparator<String>() {
|
|
||||||
@Override
|
|
||||||
public int compare(String s1, String s2) {
|
|
||||||
return s1.length() - s2.length();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for (String toUse : sortedLocaleList) {
|
|
||||||
localesToUse.add(toUse);
|
|
||||||
for (String l : availableLocales) {
|
|
||||||
if (l.equals(toUse.split("-")[0])) {
|
|
||||||
localesToUse.add(l);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (String l : availableLocales) {
|
|
||||||
if (l.startsWith(languageTag)) {
|
|
||||||
localesToUse.add(l);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (availableLocales.contains("en-US")) {
|
|
||||||
localesToUse.add("en-US");
|
|
||||||
}
|
|
||||||
for (String l : availableLocales) {
|
|
||||||
if (l.startsWith("en")) {
|
|
||||||
localesToUse.add(l);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String l : localesToUse) {
|
|
||||||
if (l.startsWith(languageTag)) {
|
|
||||||
isLocalized = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (localesToUse.size() > 1) {
|
|
||||||
isLocalized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String value = getLocalizedEntry(localized, localesToUse, "whatsNew");
|
String value = getLocalizedEntry(localized, localesToUse, "whatsNew");
|
||||||
if (!TextUtils.isEmpty(value)) {
|
if (!TextUtils.isEmpty(value)) {
|
||||||
whatsNew = value;
|
whatsNew = value;
|
||||||
@ -625,58 +553,58 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the right localized version of this entry, based on an immitation of
|
* Sets the boolean flag {@link #isLocalized} if this app entry has an localized
|
||||||
* the logic that Android/Java uses. On Android >= 24, this can get the
|
* entry in one of the user's current locales.
|
||||||
* "Language Priority List", but it doesn't always seem to be properly sorted.
|
*
|
||||||
* So this method has to kind of fake it by using {@link Locale#getDefault()}
|
* @see org.fdroid.fdroid.views.main.WhatsNewViewBinder#onCreateLoader(int, android.os.Bundle)
|
||||||
* as the first entry, then sorting the rest based on length (e.g. {@code de-AT}
|
*/
|
||||||
* before {@code de}).
|
private void setIsLocalized(Set<String> supportedLocales) {
|
||||||
|
isLocalized = false;
|
||||||
|
for (int i = 0; i < systemLocaleList.size(); i++) {
|
||||||
|
String language = systemLocaleList.get(i).getLanguage();
|
||||||
|
for (String supportedLocale : supportedLocales) {
|
||||||
|
if (language.equals(supportedLocale.split("-")[0])) {
|
||||||
|
isLocalized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the right localized version of this entry, based on an imitation of
|
||||||
|
* the logic that Android uses.
|
||||||
*
|
*
|
||||||
* @see LocaleList
|
* @see LocaleList
|
||||||
* @see Locale#getDefault()
|
|
||||||
* @see java.util.Locale.LanguageRange
|
|
||||||
*/
|
*/
|
||||||
private String getLocalizedEntry(Map<String, Map<String, Object>> localized,
|
private String getLocalizedEntry(Map<String, Map<String, Object>> localized,
|
||||||
Set<String> locales, String key) {
|
Set<String> locales, @NonNull String key) {
|
||||||
try {
|
Map<String, Object> localizedLocaleMap = getLocalizedLocaleMap(localized, locales, key);
|
||||||
for (String locale : locales) {
|
if (localizedLocaleMap != null && !localizedLocaleMap.isEmpty()) {
|
||||||
if (localized.containsKey(locale)) {
|
for (Object entry : localizedLocaleMap.values()) {
|
||||||
String value = (String) localized.get(locale).get(key);
|
return (String) entry; // NOPMD
|
||||||
if (value != null) {
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
Utils.debugLog(TAG, e.getMessage());
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getLocalizedGraphicsEntry(Map<String, Map<String, Object>> localized,
|
private String getLocalizedGraphicsEntry(Map<String, Map<String, Object>> localized,
|
||||||
Set<String> locales, String key) {
|
Set<String> locales, String key) {
|
||||||
try {
|
Map<String, Object> localizedLocaleMap = getLocalizedLocaleMap(localized, locales, key);
|
||||||
for (String locale : locales) {
|
if (localizedLocaleMap != null && !localizedLocaleMap.isEmpty()) {
|
||||||
Map<String, Object> entry = localized.get(locale);
|
for (String locale : localizedLocaleMap.keySet()) {
|
||||||
if (entry != null) {
|
return locale + "/" + localizedLocaleMap.get(locale); // NOPMD
|
||||||
Object value = entry.get(key);
|
|
||||||
if (value != null && value.toString().length() > 0) {
|
|
||||||
return locale + "/" + value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
Utils.debugLog(TAG, e.getMessage());
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String[] getLocalizedListEntry(Map<String, Map<String, Object>> localized,
|
private String[] getLocalizedListEntry(Map<String, Map<String, Object>> localized,
|
||||||
Set<String> locales, String key) {
|
Set<String> locales, String key) {
|
||||||
try {
|
Map<String, Object> localizedLocaleMap = getLocalizedLocaleMap(localized, locales, key);
|
||||||
for (String locale : locales) {
|
if (localizedLocaleMap != null && !localizedLocaleMap.isEmpty()) {
|
||||||
if (localized.containsKey(locale)) {
|
for (String locale : localizedLocaleMap.keySet()) {
|
||||||
ArrayList<String> entry = (ArrayList<String>) localized.get(locale).get(key);
|
ArrayList<String> entry = (ArrayList<String>) localizedLocaleMap.get(locale);
|
||||||
if (entry != null && entry.size() > 0) {
|
if (entry != null && entry.size() > 0) {
|
||||||
String[] result = new String[entry.size()];
|
String[] result = new String[entry.size()];
|
||||||
int i = 0;
|
int i = 0;
|
||||||
@ -688,12 +616,87 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ClassCastException e) {
|
|
||||||
Utils.debugLog(TAG, e.getMessage());
|
|
||||||
}
|
|
||||||
return new String[0];
|
return new String[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return one matching entry from the {@code localized} block in the app entry
|
||||||
|
* in the index JSON.
|
||||||
|
*/
|
||||||
|
private Map<String, Object> getLocalizedLocaleMap(Map<String, Map<String, Object>> localized,
|
||||||
|
Set<String> locales, String key) {
|
||||||
|
String[] localesToUse = getLocalesForKey(localized, locales, key);
|
||||||
|
if (localesToUse.length > 0) {
|
||||||
|
Locale firstMatch = systemLocaleList.getFirstMatch(localesToUse);
|
||||||
|
if (firstMatch != null) {
|
||||||
|
for (String languageTag : new String[]{firstMatch.toLanguageTag(), null}) {
|
||||||
|
if (languageTag == null) {
|
||||||
|
languageTag = getFallbackLanguageTag(firstMatch, localesToUse); // NOPMD
|
||||||
|
}
|
||||||
|
Map<String, Object> localeEntry = localized.get(languageTag);
|
||||||
|
if (localeEntry != null && localeEntry.containsKey(key)) {
|
||||||
|
Object value = localeEntry.get(key);
|
||||||
|
if (value != null) {
|
||||||
|
Map<String, Object> localizedLocaleMap = new HashMap<>();
|
||||||
|
localizedLocaleMap.put(languageTag, value);
|
||||||
|
return localizedLocaleMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all locales that have an entry for {@code key}.
|
||||||
|
*/
|
||||||
|
private String[] getLocalesForKey(Map<String, Map<String, Object>> localized,
|
||||||
|
Set<String> locales, String key) {
|
||||||
|
Set<String> localesToUse = new HashSet<>();
|
||||||
|
for (String locale : locales) {
|
||||||
|
Map<String, Object> localeEntry = localized.get(locale);
|
||||||
|
if (localeEntry != null && localeEntry.get(key) != null) {
|
||||||
|
localesToUse.add(locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return localesToUse.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look for the first language-country match for languages with multiple scripts.
|
||||||
|
* Then look for a language-only match, for when there is no exact
|
||||||
|
* {@link Locale} match. Then try a locale with the same language, but
|
||||||
|
* different country. If there are still no matches, return the {@code en-US}
|
||||||
|
* entry. If all else fails, try to return the first existing English locale.
|
||||||
|
*/
|
||||||
|
private String getFallbackLanguageTag(Locale firstMatch, String[] localesToUse) {
|
||||||
|
final String firstMatchLanguageCountry = firstMatch.getLanguage() + "-" + firstMatch.getCountry();
|
||||||
|
for (String languageTag : localesToUse) {
|
||||||
|
if (languageTag.equals(firstMatchLanguageCountry)) {
|
||||||
|
return languageTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final String firstMatchLanguage = firstMatch.getLanguage();
|
||||||
|
String englishLastResort = null;
|
||||||
|
for (String languageTag : localesToUse) {
|
||||||
|
if (languageTag.equals(firstMatchLanguage)) {
|
||||||
|
return languageTag;
|
||||||
|
} else if ("en-US".equals(languageTag)) {
|
||||||
|
englishLastResort = languageTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String languageTag : localesToUse) {
|
||||||
|
String languageToUse = languageTag.split("-")[0];
|
||||||
|
if (firstMatchLanguage.equals(languageToUse)) {
|
||||||
|
return languageTag;
|
||||||
|
} else if (englishLastResort == null && "en".equals(languageToUse)) {
|
||||||
|
englishLastResort = languageTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return englishLastResort;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the app description text with all newlines replaced by {@code <br>}
|
* Returns the app description text with all newlines replaced by {@code <br>}
|
||||||
*/
|
*/
|
||||||
|
@ -209,6 +209,13 @@ public interface Schema {
|
|||||||
String TV_SCREENSHOTS = "tvScreenshots";
|
String TV_SCREENSHOTS = "tvScreenshots";
|
||||||
String WEAR_SCREENSHOTS = "wearScreenshots";
|
String WEAR_SCREENSHOTS = "wearScreenshots";
|
||||||
String IS_APK = "isApk";
|
String IS_APK = "isApk";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has this {@code App} been localized into one of the user's current locales.
|
||||||
|
*
|
||||||
|
* @see App#setIsLocalized(java.util.Set)
|
||||||
|
* @see org.fdroid.fdroid.views.main.WhatsNewViewBinder#onCreateLoader(int, android.os.Bundle)
|
||||||
|
*/
|
||||||
String IS_LOCALIZED = "isLocalized";
|
String IS_LOCALIZED = "isLocalized";
|
||||||
|
|
||||||
interface AutoInstallApk {
|
interface AutoInstallApk {
|
||||||
|
@ -6,6 +6,7 @@ import android.content.ContentValues;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import androidx.core.os.LocaleListCompat;
|
||||||
import org.fdroid.fdroid.Preferences;
|
import org.fdroid.fdroid.Preferences;
|
||||||
import org.fdroid.fdroid.TestUtils;
|
import org.fdroid.fdroid.TestUtils;
|
||||||
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
|
import org.fdroid.fdroid.data.Schema.AppMetadataTable.Cols;
|
||||||
@ -303,7 +304,7 @@ public class AppProviderTest extends FDroidProviderTest {
|
|||||||
localized.put("es", es);
|
localized.put("es", es);
|
||||||
localized.put("fr", fr);
|
localized.put("fr", fr);
|
||||||
|
|
||||||
Locale.setDefault(new Locale("nl", "NL"));
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("nl-NL");
|
||||||
app.setLocalized(localized);
|
app.setLocalized(localized);
|
||||||
assertFalse(app.isLocalized);
|
assertFalse(app.isLocalized);
|
||||||
|
|
||||||
@ -324,7 +325,7 @@ public class AppProviderTest extends FDroidProviderTest {
|
|||||||
app.setLocalized(localized);
|
app.setLocalized(localized);
|
||||||
assertFalse(app.isLocalized);
|
assertFalse(app.isLocalized);
|
||||||
|
|
||||||
Locale.setDefault(new Locale("en", "US"));
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-US");
|
||||||
app = new App();
|
app = new App();
|
||||||
localized.clear();
|
localized.clear();
|
||||||
localized.put("en-US", en);
|
localized.put("en-US", en);
|
||||||
|
@ -1,23 +1,27 @@
|
|||||||
package org.fdroid.fdroid.data;
|
package org.fdroid.fdroid.data;
|
||||||
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Build;
|
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.fdroid.fdroid.TestUtils;
|
||||||
|
import org.junit.Assume;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.RobolectricTestRunner;
|
import org.robolectric.RobolectricTestRunner;
|
||||||
|
import org.robolectric.shadows.ShadowLog;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
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)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
@SuppressWarnings("LocalVariableName")
|
@SuppressWarnings("LocalVariableName")
|
||||||
@ -25,83 +29,25 @@ public class LocaleSelectionTest {
|
|||||||
|
|
||||||
private static final String KEY = "summary";
|
private static final String KEY = "summary";
|
||||||
|
|
||||||
@Test
|
private static final String EN_US_NAME = "Checkey: info on local apps\n";
|
||||||
public void correctLocaleSelectionBeforeSDK24() throws Exception {
|
private static final String EN_US_FEATURE_GRAPHIC = "en-US/featureGraphic.png";
|
||||||
TestUtils.setFinalStatic(Build.VERSION.class.getDeclaredField("SDK_INT"), 19);
|
private static final String EN_US_PHONE_SCREENSHOT = "en-US/phoneScreenshots/First.png";
|
||||||
assertTrue(Build.VERSION.SDK_INT < 24);
|
private static final String EN_US_SEVEN_INCH_SCREENSHOT = "en-US/sevenInchScreenshots/checkey-tablet.png";
|
||||||
App app;
|
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<>();
|
@Before
|
||||||
HashMap<String, Object> en_US = new HashMap<>();
|
public final void setUp() {
|
||||||
en_US.put(KEY, "summary-en_US");
|
ShadowLog.stream = System.out;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void correctLocaleSelectionFromSDK24() throws Exception {
|
public void localeSelection() throws Exception {
|
||||||
|
|
||||||
TestUtils.setFinalStatic(Build.VERSION.class.getDeclaredField("SDK_INT"), 29);
|
App app = new App();
|
||||||
assertTrue(Build.VERSION.SDK_INT >= 24);
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-US,de-DE");
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
//no metadata present
|
//no metadata present
|
||||||
Map<String, Map<String, Object>> localized = new HashMap<>();
|
Map<String, Map<String, Object>> localized = new HashMap<>();
|
||||||
@ -112,10 +58,18 @@ public class LocaleSelectionTest {
|
|||||||
en_US.put(KEY, "summary-en_US");
|
en_US.put(KEY, "summary-en_US");
|
||||||
HashMap<String, Object> en_GB = new HashMap<>();
|
HashMap<String, Object> en_GB = new HashMap<>();
|
||||||
en_GB.put(KEY, "summary-en_GB");
|
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<>();
|
HashMap<String, Object> de_AT = new HashMap<>();
|
||||||
de_AT.put(KEY, "summary-de_AT");
|
de_AT.put(KEY, "summary-de_AT");
|
||||||
HashMap<String, Object> de_DE = new HashMap<>();
|
HashMap<String, Object> de_DE = new HashMap<>();
|
||||||
de_DE.put(KEY, "summary-de_DE");
|
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";
|
app.summary = "reset";
|
||||||
localized.put("de-AT", de_AT);
|
localized.put("de-AT", de_AT);
|
||||||
@ -125,8 +79,7 @@ public class LocaleSelectionTest {
|
|||||||
// just select the matching en-US locale, nothing special here
|
// just select the matching en-US locale, nothing special here
|
||||||
assertEquals(en_US.get(KEY), app.summary);
|
assertEquals(en_US.get(KEY), app.summary);
|
||||||
|
|
||||||
Locale.setDefault(new Locale("en", "SE"));
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-SE,de-DE");
|
||||||
when(localeList.toLanguageTags()).thenReturn("en-SE,de-DE");
|
|
||||||
app.setLocalized(localized);
|
app.setLocalized(localized);
|
||||||
// Fall back to another en locale before de
|
// Fall back to another en locale before de
|
||||||
assertEquals(en_US.get(KEY), app.summary);
|
assertEquals(en_US.get(KEY), app.summary);
|
||||||
@ -138,8 +91,7 @@ public class LocaleSelectionTest {
|
|||||||
localized.put("en-GB", en_GB);
|
localized.put("en-GB", en_GB);
|
||||||
localized.put("en-US", en_US);
|
localized.put("en-US", en_US);
|
||||||
|
|
||||||
Locale.setDefault(new Locale("de", "AT"));
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("de-AT,de-DE");
|
||||||
when(localeList.toLanguageTags()).thenReturn("de-AT,de-DE");
|
|
||||||
app.setLocalized(localized);
|
app.setLocalized(localized);
|
||||||
// full match against a non-default locale
|
// full match against a non-default locale
|
||||||
assertEquals(de_AT.get(KEY), app.summary);
|
assertEquals(de_AT.get(KEY), app.summary);
|
||||||
@ -147,14 +99,13 @@ public class LocaleSelectionTest {
|
|||||||
app.summary = "reset";
|
app.summary = "reset";
|
||||||
localized.clear();
|
localized.clear();
|
||||||
localized.put("de-AT", de_AT);
|
localized.put("de-AT", de_AT);
|
||||||
localized.put("de", de_DE);
|
localized.put("de", de);
|
||||||
localized.put("en-GB", en_GB);
|
localized.put("en-GB", en_GB);
|
||||||
localized.put("en-US", en_US);
|
localized.put("en-US", en_US);
|
||||||
|
|
||||||
Locale.setDefault(new Locale("de", "CH"));
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("de-CH,en-US");
|
||||||
when(localeList.toLanguageTags()).thenReturn("de-CH,en-US");
|
|
||||||
app.setLocalized(localized);
|
app.setLocalized(localized);
|
||||||
assertEquals(de_DE.get(KEY), app.summary);
|
assertEquals(de.get(KEY), app.summary);
|
||||||
|
|
||||||
app.summary = "reset";
|
app.summary = "reset";
|
||||||
localized.clear();
|
localized.clear();
|
||||||
@ -162,13 +113,12 @@ public class LocaleSelectionTest {
|
|||||||
localized.put("en-US", en_US);
|
localized.put("en-US", en_US);
|
||||||
|
|
||||||
Locale.setDefault(new Locale("en", "AU"));
|
Locale.setDefault(new Locale("en", "AU"));
|
||||||
when(localeList.toLanguageTags()).thenReturn("en-AU");
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("en-AU");
|
||||||
app.setLocalized(localized);
|
app.setLocalized(localized);
|
||||||
assertEquals(en_US.get(KEY), app.summary);
|
assertEquals(en_US.get(KEY), app.summary);
|
||||||
|
|
||||||
app.summary = "reset";
|
app.summary = "reset";
|
||||||
Locale.setDefault(new Locale("zh", "TW", "#Hant"));
|
App.systemLocaleList = LocaleListCompat.forLanguageTags("zh-Hant-TW,zh-Hans-CN");
|
||||||
when(localeList.toLanguageTags()).thenReturn("zh-Hant-TW,zh-Hans-CN");
|
|
||||||
localized.clear();
|
localized.clear();
|
||||||
localized.put("en", en_GB);
|
localized.put("en", en_GB);
|
||||||
localized.put("en-US", en_US);
|
localized.put("en-US", en_US);
|
||||||
@ -197,5 +147,107 @@ public class LocaleSelectionTest {
|
|||||||
localized.put("zh-CN", zh_CN);
|
localized.put("zh-CN", zh_CN);
|
||||||
app.setLocalized(localized);
|
app.setLocalized(localized);
|
||||||
assertEquals(zh_CN.get(KEY), app.summary);
|
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",
|
"isLocalized",
|
||||||
"preferredSigner",
|
"preferredSigner",
|
||||||
"prefs",
|
"prefs",
|
||||||
|
"systemLocaleList",
|
||||||
"TAG",
|
"TAG",
|
||||||
};
|
};
|
||||||
runJsonIgnoreTest(new App(), allowedInApp, ignoredInApp);
|
runJsonIgnoreTest(new App(), allowedInApp, ignoredInApp);
|
||||||
|
285
app/src/test/resources/localized.json
Normal file
285
app/src/test/resources/localized.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user