test all formats in all languages

We've had a number of crashes due to bad formats in various
translated strings.  This test 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.

I couldn't get the Resources stuff working in Robolectric, so I
made this an emulator test.

The change to the Swedish translation included in this commit are
fixes for issues that these tests found.

closes #923
This commit is contained in:
Hans-Christoph Steiner 2017-04-18 20:32:51 +02:00
parent 670e6be39a
commit 3a194026fa
2 changed files with 212 additions and 2 deletions

View File

@ -0,0 +1,210 @@
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.ArrayList;
import java.util.Collections;
import java.util.HashMap;
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 ArrayList<String> localeNames = new ArrayList<>(locales.length);
private AssetManager assets;
private Configuration config;
private Resources resources;
@Before
public void setUp() {
for (Locale locale : locales) {
localeNames.add(locale.toString());
}
Collections.sort(localeNames);
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

@ -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>