Merge branch 'localization-fixes' into 'master'

localization fixes

Closes #923

See merge request !484
This commit is contained in:
Hans-Christoph Steiner 2017-04-18 20:04:22 +00:00
commit b2d89ec665
7 changed files with 238 additions and 10 deletions

View File

@ -0,0 +1,211 @@
package org.fdroid.fdroid;
import android.app.Instrumentation;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Runs through all of the translated strings and tests them with the same format
* values that the source strings expect. This is to ensure that the formats in
* the translations are correct in number and in type (e.g. {@code s} or {@code s}.
* It reads the source formats and then builds {@code formats} to represent the
* position and type of the formats. Then it runs through all of the translations
* with formats of the correct number and type.
*/
@RunWith(AndroidJUnit4.class)
public class LocalizationTest {
public static final String TAG = "LocalizationTest";
private final Pattern androidFormat = Pattern.compile("(%[a-z0-9]\\$?[a-z]?)");
private final Locale[] locales = Locale.getAvailableLocales();
private final HashSet<String> localeNames = new HashSet<>(locales.length);
private AssetManager assets;
private Configuration config;
private Resources resources;
@Before
public void setUp() {
for (Locale locale : Languages.LOCALES_TO_TEST) {
localeNames.add(locale.toString());
}
for (Locale locale : locales) {
localeNames.add(locale.toString());
}
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
Context context = instrumentation.getTargetContext();
assets = context.getAssets();
config = context.getResources().getConfiguration();
config.locale = Locale.ENGLISH;
// Resources() requires DisplayMetrics, but they are only needed for drawables
resources = new Resources(assets, new DisplayMetrics(), config);
}
@Test
public void testLoadAllPlural() throws IllegalAccessException {
Field[] fields = R.plurals.class.getDeclaredFields();
HashMap<String, String> haveFormats = new HashMap<>();
for (Field field : fields) {
//Log.i(TAG, field.getName());
int resId = field.getInt(int.class);
CharSequence string = resources.getQuantityText(resId, 4);
//Log.i(TAG, field.getName() + ": '" + string + "'");
Matcher matcher = androidFormat.matcher(string);
int matches = 0;
char[] formats = new char[5];
while (matcher.find()) {
String match = matcher.group(0);
char formatType = match.charAt(match.length() - 1);
switch (match.length()) {
case 2:
formats[matches] = formatType;
matches++;
break;
case 4:
formats[Integer.parseInt(match.substring(1, 2)) - 1] = formatType;
break;
case 5:
formats[Integer.parseInt(match.substring(1, 3)) - 1] = formatType;
break;
default:
throw new IllegalStateException(field.getName() + " has bad format: " + match);
}
}
haveFormats.put(field.getName(), new String(formats).trim());
}
for (Locale locale : locales) {
config.locale = locale;
// Resources() requires DisplayMetrics, but they are only needed for drawables
resources = new Resources(assets, new DisplayMetrics(), config);
for (Field field : fields) {
//Log.i(TAG, field.getName());
int resId = field.getInt(int.class);
for (int quantity = 0; quantity < 22; quantity++) {
resources.getQuantityString(resId, quantity);
}
String formats = haveFormats.get(field.getName());
String formattedString = null;
switch (formats) {
case "d":
formattedString = resources.getQuantityString(resId, 1, 1);
break;
case "s":
formattedString = resources.getQuantityString(resId, 1, "ONE");
break;
case "ds":
formattedString = resources.getQuantityString(resId, 2, 1, "TWO");
break;
default:
if (!TextUtils.isEmpty(formats)) {
throw new IllegalStateException("Pattern not included in tests: " + formats);
}
}
if (formattedString != null) { // NOPMD
//Log.i(TAG, locale + " " + field.getName() + " FORMATTED: " + formattedString);
}
//Log.i(TAG, field.getName() + ": " + string);
}
}
}
@Test
public void testLoadAllStrings() throws IllegalAccessException {
Field[] fields = R.string.class.getDeclaredFields();
HashMap<String, String> haveFormats = new HashMap<>();
for (Field field : fields) {
String string = resources.getString(field.getInt(int.class));
Matcher matcher = androidFormat.matcher(string);
int matches = 0;
char[] formats = new char[5];
while (matcher.find()) {
String match = matcher.group(0);
char formatType = match.charAt(match.length() - 1);
switch (match.length()) {
case 2:
formats[matches] = formatType;
matches++;
break;
case 4:
formats[Integer.parseInt(match.substring(1, 2)) - 1] = formatType;
break;
case 5:
formats[Integer.parseInt(match.substring(1, 3)) - 1] = formatType;
break;
default:
throw new IllegalStateException(field.getName() + " has bad format: " + match);
}
}
haveFormats.put(field.getName(), new String(formats).trim());
}
//for (Locale locale : new Locale[]{new Locale("es")}) {
for (Locale locale : locales) {
config.locale = locale;
// Resources() requires DisplayMetrics, but they are only needed for drawables
resources = new Resources(assets, new DisplayMetrics(), config);
for (Field field : fields) {
//Log.i(TAG, field.getName());
int resId = field.getInt(int.class);
resources.getString(resId);
String formats = haveFormats.get(field.getName());
String formattedString = null;
switch (formats) {
case "d":
formattedString = resources.getString(resId, 1);
break;
case "dd":
formattedString = resources.getString(resId, 1, 2);
break;
case "s":
formattedString = resources.getString(resId, "ONE");
break;
case "ss":
formattedString = resources.getString(resId, "ONE", "TWO");
break;
case "sss":
formattedString = resources.getString(resId, "ONE", "TWO", "THREE");
break;
case "ssss":
formattedString = resources.getString(resId, "ONE", "TWO", "THREE", "FOUR");
break;
case "ssd":
formattedString = resources.getString(resId, "ONE", "TWO", 3);
break;
case "sssd":
formattedString = resources.getString(resId, "ONE", "TWO", "THREE", 4);
break;
default:
if (!TextUtils.isEmpty(formats)) {
throw new IllegalStateException("Pattern not included in tests: " + formats);
}
}
if (formattedString != null) { //NOPMD
//Log.i(TAG, "FORMATTED: " + formattedString);
}
//Log.i(TAG, field.getName() + ": " + string);
}
}
}
}

View File

@ -203,7 +203,7 @@ public final class Languages {
return Character.toUpperCase(line.charAt(0)) + line.substring(1);
}
private static final Locale[] LOCALES_TO_TEST = {
public static final Locale[] LOCALES_TO_TEST = {
Locale.ENGLISH,
Locale.FRENCH,
Locale.GERMAN,

View File

@ -15,11 +15,9 @@ import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.io.filefilter.RegexFileFilter;
import org.fdroid.fdroid.AppFilter;
import org.fdroid.fdroid.FDroidApp;
@ -40,10 +38,10 @@ import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
@ -377,7 +375,7 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
String languageTag = defaultLocale.getLanguage();
String localeTag = languageTag + "-" + defaultLocale.getCountry();
Set<String> locales = localized.keySet();
Set<String> localesToUse = new TreeSet<>();
Set<String> localesToUse = new LinkedHashSet<>();
if (locales.contains(localeTag)) {
localesToUse.add(localeTag);
@ -398,11 +396,14 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
}
}
// if key starts with Upper case, its set by humans
video = getLocalizedEntry(localized, localesToUse, "Video");
String value = getLocalizedEntry(localized, localesToUse, "Video");
if (!TextUtils.isEmpty(value)) {
video = value.split("\n", 1)[0];
}
whatsNew = getLocalizedEntry(localized, localesToUse, "WhatsNew");
// Name, Summary, Description existed before localization so they shouldn't replace
// non-localized old data format with a null or blank string
String value = getLocalizedEntry(localized, localesToUse, "Name");
value = getLocalizedEntry(localized, localesToUse, "Name");
if (!TextUtils.isEmpty(value)) {
name = value;
}
@ -432,7 +433,10 @@ public class App extends ValueObject implements Comparable<App>, Parcelable {
try {
for (String locale : locales) {
if (localized.containsKey(locale)) {
return (String) localized.get(locale).get(key);
String value = (String) localized.get(locale).get(key);
if (value != null) {
return value;
}
}
}
} catch (ClassCastException e) {

View File

@ -740,6 +740,11 @@ public class AppDetailsRecyclerViewAdapter
updateExpandableItem(false);
contentView.removeAllViews();
// Video link
if (uriIsSetAndCanBeOpened(app.video)) {
addLinkItemView(contentView, R.string.menu_video, R.drawable.ic_video, app.video);
}
// Source button
if (uriIsSetAndCanBeOpened(app.sourceCode)) {
addLinkItemView(contentView, R.string.menu_source, R.drawable.ic_source_code, app.sourceCode);

View File

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#666666" android:pathData="M10,16.5V7.5L16,12M20,4.4C19.4,4.2 15.7,4 12,4C8.3,4 4.6,4.19 4,4.38C2.44,4.9 2,8.4 2,12C2,15.59 2.44,19.1 4,19.61C4.6,19.81 8.3,20 12,20C15.7,20 19.4,19.81 20,19.61C21.56,19.1 22,15.59 22,12C22,8.4 21.56,4.91 20,4.4Z" />
</vector>

View File

@ -481,8 +481,8 @@
<string name="tts_category_name">Kategori %1$s</string>
<plurals name="tts_view_all_in_category">
<item quantity="one">Visa enda appen i %2$d kategorin</item>
<item quantity="other">Visa alla %2$d appar från %2$s kategorin</item>
<item quantity="one">Visa enda appen i %2$s kategorin</item>
<item quantity="other">Visa alla %1$d appar från %2$s kategorin</item>
</plurals>
<string name="app__install_downloaded_update">Uppdatera</string>

View File

@ -159,6 +159,7 @@
<string name="menu_email">E-Mail Author</string>
<string name="menu_issues">Issues</string>
<string name="menu_changelog">Changelog</string>
<string name="menu_video">Video</string>
<string name="menu_source">Source Code</string>
<string name="menu_upgrade">Upgrade</string>
<string name="menu_donate">Donate</string>