package org.wikipedia.test; import android.content.res.AssetManager; import android.content.res.Configuration; import android.content.res.Resources; import android.support.annotation.NonNull; import android.support.annotation.PluralsRes; import android.support.annotation.StringRes; import android.util.DisplayMetrics; import org.junit.Test; import org.wikipedia.R; import org.wikipedia.model.BaseModel; import org.wikipedia.util.ConfigurationCompat; import org.wikipedia.util.log.L; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import static android.support.test.InstrumentationRegistry.getTargetContext; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.Assert.fail; /** * Tests to make sure that the string resources don't cause any issues. Mainly the goal is to test * all translations, but even the default strings are tested. * * TODO: check content_license_html is valid HTML */ public class TranslationTests { /** Add more if needed, but then also add some tests. */ private static final String[] POSSIBLE_PARAMS = new String[] {"%s", "%1$s", "%2$s", "%1$d", "%2$d", "%d", "%.2f", "^1"}; private final StringBuilder mismatches = new StringBuilder(); @Test public void testAllTranslations() { setLocale(Locale.ROOT.toString()); String defaultLang = Locale.getDefault().getLanguage(); List<Res> tagRes = new ResourceCollector("<", "<").collectParameterResources(defaultLang); List<Res> noTagRes = new ResourceCollector("<", "<").not().collectParameterResources(defaultLang); List<Res> stringParamRes = new ResourceCollector("%s").collectParameterResources(defaultLang); List<Res> twoStringParamRes = new ResourceCollector("%2$s").collectParameterResources(defaultLang); List<Res> twoDecimalParamRes = new ResourceCollector("%2$d").collectParameterResources(defaultLang); List<Res> decimalParamRes = new ResourceCollector("%d").collectParameterResources(defaultLang); List<Res> floatParamRes = new ResourceCollector("%.2f").collectParameterResources(defaultLang); List<Res> textUtilTemplateParamRes = new ResourceCollector("^1").collectParameterResources(defaultLang); List<Res> pluralRes = collectPluralResources(); // todo: flag usage of templates {{}}. AssetManager assetManager = getResources().getAssets(); for (String lang : assetManager.getLocales()) { L.i("----locale=" + (lang.equals("") ? "DEFAULT" : lang)); setLocale(lang); checkAllStrings(lang); // commented out during the transition from 1 param to 0 // checkTranslationHasNoParameter(R.string.saved_pages_search_empty_message); // checkTranslationHasNoParameter(R.string.history_search_empty_message); if (!lang.startsWith("qq")) { // tag (html) parameters for (Res res : tagRes) { if (res.id == R.string.wp_stylized && (lang.startsWith("iw") || lang.startsWith("he") || lang.startsWith("ckb")) || res.id == R.string.notification_talk || res.id == R.string.notification_reverted || res.id == R.string.notification_thanks) { // exceptions of the rule continue; } expectContains(res, "<", "<"); } for (Res res : noTagRes) { expectNotContains(res, "<", "<"); } // string parameters for (Res res : stringParamRes) { checkTranslationHasParameter(res, "%s", "[stringParam]", null); } // 2 string parameters for (Res res : twoStringParamRes) { checkTranslationHasTwoParameters(res, "%s", "[stringParam1]", "[stringParam2]"); } // decimal parameters for (Res res : decimalParamRes) { final int param1 = 42; checkTranslationHasParameter(res, "%d", param1, null); } // 2 decimal parameters for (Res res : twoDecimalParamRes) { final int param1 = 42; final int param2 = 8675309; checkTranslationHasTwoParameters(res, "%d", param1, param2); } // floating point parameters for (Res res : floatParamRes) { final float param1 = .27f; checkTranslationHasParameter(res, "%.2f", param1, "0,27"); } // template format for com.android.TextUtils.expandTemplate for (Res res : textUtilTemplateParamRes) { checkTranslationHasParameter(res, "^1", "[templateParam]", null); } for (Res res : pluralRes) { checkPluralHasOther(res); } } } assertThat("\n" + mismatches.toString(), mismatches.length(), is(0)); } private Locale myLocale; private void setLocale(String lang) { myLocale = new Locale(lang); Locale.setDefault(myLocale); DisplayMetrics dm = getResources().getDisplayMetrics(); Configuration conf = getResources().getConfiguration(); ConfigurationCompat.setLocale(conf, myLocale); getResources().updateConfiguration(conf, dm); } private void checkAllStrings(String lang) { new ResourceCollector().collectParameterResources(lang); } private String buildLogString(int i, String name) { return myLocale + "-" + i + "; name = " + name; } private void expectNotContains(Res res, String... examples) { String translatedString = getString(res.id); // L.i(myLocale + ":" + translatedString); for (String example : examples) { if (translatedString.contains(example)) { final String msg = myLocale + ":" + res.name + " = " + translatedString + "' contains " + example; L.e(msg); mismatches.append(msg).append("\n"); break; } } } private void expectContains(Res res, Object... examples) { String translatedString = getString(res.id); // L.i(myLocale + ":" + translatedString); boolean found = false; for (Object example : examples) { if (translatedString.contains(example.toString())) { found = true; break; } } if (!found) { final String msg = myLocale + ":" + res.name + " = " + translatedString + "' does not contain " + Arrays.toString(examples); L.e(msg); mismatches.append(msg).append("\n"); } } private void checkTranslationHasNoParameter(Res res) { final String val1 = "[val1]"; String translatedString = getString(res.id, val1); // L.i(myLocale + ":" + translatedString); if (translatedString.contains(val1)) { final String msg = myLocale + ":" + res.name + " = " + translatedString + "' contains " + val1; L.e(msg); mismatches.append(msg).append("\n"); } } private void checkTranslationHasParameter(Res res, String paramName, Object val1, String alternateFormat) { // L.i(myLocale + ":" + res.name + ":" + paramName); String translatedString = getString(res.id, val1); // L.d(translatedString); if (!translatedString.contains(String.format(paramName, val1)) && (alternateFormat == null || !translatedString.contains(alternateFormat))) { final String msg = myLocale + ":" + res.name + " = " + translatedString + "' is missing " + val1; L.e(msg); mismatches.append(msg).append("\n"); } } private void checkTranslationHasTwoParameters(Res res, String paramName, Object val1, Object val2) { L.i(myLocale + ":" + res.name + ":" + paramName); String translatedString = getString(res.id, val1, val2); L.d(translatedString); if (!translatedString.contains(String.format(paramName, val1)) || !translatedString.contains(String.format(paramName, val2))) { final String msg = myLocale + ":" + res.name + " = " + translatedString + "' is missing " + val1 + "' or " + val2; L.e(msg); mismatches.append(msg).append("\n"); } } private void checkPluralHasOther(Res res) { L.i(myLocale + ":" + res.name); try { final int paramOther = 42; getQuantityString(res.id, 0); getQuantityString(res.id, 1); getQuantityString(res.id, 2); getQuantityString(res.id, paramOther); } catch (Exception e) { final String msg = myLocale + ":" + res.name + " plural is missing 'other'"; L.e(msg); mismatches.append(msg).append("\n"); } } @NonNull private String getString(@StringRes int id, Object... args) { return getTargetContext().getString(id, args); } @NonNull private String getString(@StringRes int id) { return getTargetContext().getString(id); } @NonNull private String getQuantityString(@PluralsRes int id, int quantity) { return getResources().getQuantityString(id, quantity); } @NonNull private Resources getResources() { return getTargetContext().getResources(); } private class ResourceCollector { private boolean negate; private final String[] paramExamples; ResourceCollector(String... paramExamples) { this.paramExamples = paramExamples; } private ResourceCollector not() { negate = true; return this; } private List<Res> collectParameterResources(String lang) { final List<Res> resources = new ArrayList<>(); final R.string stringResources = new R.string(); final Class<R.string> c = R.string.class; final Field[] fields = c.getDeclaredFields(); for (int i = 0, max = fields.length; i < max; i++) { final String name; final int resourceId; try { name = fields[i].getName(); resourceId = fields[i].getInt(stringResources); } catch (Exception e) { L.e(myLocale + "-" + i + "; failed: " + e.getMessage()); continue; } // todo: don't try. Die. try { String value = getString(resourceId); // don't care about appcompat string; and preference string resources don't get translated if (name.startsWith("abc_") || name.startsWith("preference_") // Required after upgrading Support Libraries from v23.0.1 to v23.1.0. || name.equals("character_counter_pattern") || name.startsWith("hockeyapp_") || name.equals("find_in_page_result")) { continue; } assertParameterFormats(lang, name, value); // Find parameter boolean found = findParameter(value); if ((!negate && found) || (negate && !found)) { resources.add(new Res(resourceId, name)); } } catch (Resources.NotFoundException e) { L.w(buildLogString(i, name) + "; <not found>"); } catch (RuntimeException e) { L.e(buildLogString(i, name) + "; --- " + e.getMessage()); } } return resources; } /** * If it has a parameter it should be one of POSSIBLE_PARAMS. * If not then flag this so we can improve the tests. */ private void assertParameterFormats(String lang, String name, String value) { if (value.startsWith("Last updated")) { L.d(""); } if (value.contains("%")) { boolean ok = false; int start = value.indexOf('%'); for (String possible : POSSIBLE_PARAMS) { int end = value.indexOf(getLastChar(possible), start); if (end != -1 && end < value.length()) { String candidate = value.substring(start, end + 1); L.d("candidate = " + candidate); if (possible.equals(candidate)) { ok = true; break; } } } if (!ok) { fail("Unexpected format in " + name + " (" + lang + "): '" + value + "'. Update tests!"); } } } private String getLastChar(String str) { return str.substring(str.length() - 1); } private boolean findParameter(String value) { boolean found = false; for (String paramExample : paramExamples) { if (value.contains(paramExample)) { found = true; if (!negate) { break; } } } return found; } } private List<Res> collectPluralResources() { final List<Res> resources = new ArrayList<>(); final R.plurals pluralResources = new R.plurals(); final Class<R.plurals> c = R.plurals.class; final Field[] fields = c.getDeclaredFields(); for (int i = 0, max = fields.length; i < max; i++) { final String name; final int resourceId; try { name = fields[i].getName(); resourceId = fields[i].getInt(pluralResources); } catch (Exception e) { L.e(myLocale + "-" + i + "; failed: " + e.getMessage()); continue; } resources.add(new Res(resourceId, name)); } return resources; } private static class Res extends BaseModel { private final int id; private final String name; Res(int id, String name) { this.id = id; this.name = name; } } }