/** * Copyright (c) 2012 Todoroo Inc * * See the file "LICENSE" for the full license governing this code. */ package com.todoroo.andlib.test; import java.lang.reflect.Field; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; import android.content.res.Resources; import com.timsu.astrid.R; /** * Tests translations for consistency with the default values. You must * extend this class and create it with your own values for strings * and arrays. * * @author Tim Su <tim@todoroo.com> * */ abstract public class TranslationTests extends TodorooTestCase { // --- abstract methods /** * @return R.string.class */ public abstract Class<?> getStringResources(); /** * @return R.array.class */ public abstract Class<?> getArrayResources(); /** * @return array of fields that are parsed by SimpleDateFormat */ public abstract int[] getDateFormatStrings(); // --- tests private static final class FormatStringData { private static final char[] scratch = new char[10]; /** format characters */ public final char[] characters; /** the original string */ public final String string; public FormatStringData(String string) { this.string = string; int pos = -1; int count = 0; while(true) { pos = string.indexOf('%', ++pos); if(pos++ == -1) break; if(pos >= string.length()) scratch[count++] = '\0'; else scratch[count++] = string.charAt(pos); } characters = new char[count]; for(int i = 0; i < count; i++) { characters[i] = scratch[i]; } } /** test that the characters match */ public boolean matches(FormatStringData other) { if(characters.length != other.characters.length) return false; outer: for(int i = 0; i < characters.length; i++) { if(Character.isDigit(characters[i])) { for(int j = 0; j < other.characters.length; j++) if(characters[i] == other.characters[j]) break outer; return false; } else if(characters[i] != other.characters[i]) return false; } return true; } @Override public String toString() { StringBuilder value = new StringBuilder("["); for(int i = 0; i < characters.length; i++) { value.append(characters[i]); if(i < characters.length - 1) value.append(','); } value.append("]: '").append(string).append('\''); return value.toString(); } } /** * Internal test of format string parser */ public void testFormatStringParser() { String s = "abc"; FormatStringData data = new FormatStringData(s); assertEquals(s, data.string); assertEquals(0, data.characters.length); s = "abc %s def"; data = new FormatStringData(s); assertEquals(1, data.characters.length); assertEquals('s', data.characters[0]); s = "abc %%s def %d"; data = new FormatStringData(s); assertEquals(2, data.characters.length); assertEquals('%', data.characters[0]); assertEquals('d', data.characters[1]); assertTrue(data.toString(), data.toString().contains("[%")); assertTrue(data.toString(), data.toString().contains("d]")); assertTrue(data.toString(), data.toString().contains(s)); assertTrue(data.matches(new FormatStringData("espanol %% und %d si"))); assertFalse(data.matches(new FormatStringData("ingles %d ja %% pon"))); s = "% abc %"; data = new FormatStringData(s); assertEquals(2, data.characters.length); assertEquals(' ', data.characters[0]); assertEquals('\0', data.characters[1]); } /** * Test that the format specifiers in translations match exactly the * translations in the default text */ public void testFormatStringsMatch() throws Exception { final Resources r = getContext().getResources(); final int[] strings = getResourceIds(getStringResources()); final FormatStringData[] formatStrings = new FormatStringData[strings.length]; final StringBuilder failures = new StringBuilder(); for(int i = 0; i < strings.length; i++) { try { String string = r.getString(strings[i]); formatStrings[i] = new FormatStringData(string); } catch (Exception e) { String name = r.getResourceName(strings[i]); failures.append(String.format("error opening %s: %s\n", name, e.getMessage())); } } forEachLocale(new Runnable() { public void run() { Locale locale = r.getConfiguration().locale; for(int i = 0; i < strings.length; i++) { try { if (strings[i] == R.string.premium_speech_bubble_2) // Special exception--this string contains a % character continue; String string = r.getString(strings[i]); FormatStringData newFS = new FormatStringData(string); if(!newFS.matches(formatStrings[i])) { String name = r.getResourceName(strings[i]); failures.append(String.format("%s (%s): %s != %s\n", name, locale.toString(), newFS, formatStrings[i])); } } catch (Exception e) { String name = r.getResourceName(strings[i]); failures.append(String.format("%s: error opening %s: %s\n", locale.toString(), name, e.getMessage())); } } } }); assertTrue(failures.toString(), errorCount(failures) == 0); } /** * Test that date formatters parse correctly */ public void testDateFormats() throws Exception { final Resources r = getContext().getResources(); final StringBuilder failures = new StringBuilder(); final int[] dateStrings = getDateFormatStrings(); final Date date = new Date(); forEachLocale(new Runnable() { public void run() { Locale locale = r.getConfiguration().locale; for(int i = 0; i < dateStrings.length; i++) { try { String string = r.getString(dateStrings[i]); try { new SimpleDateFormat(string).format(date); } catch (Exception e) { String name = r.getResourceName(dateStrings[i]); failures.append(String.format("%s: invalid format string '%s': %s\n", locale.toString(), name, e.getMessage())); } } catch (Exception e) { String name = r.getResourceName(dateStrings[i]); failures.append(String.format("%s: error opening %s: %s\n", locale.toString(), name, e.getMessage())); } } } }); assertEquals(failures.toString(), 0, errorCount(failures)); } /** * Test that there are the same number of array entries in each locale */ public void testArraySizesMatch() throws Exception { final Resources r = getContext().getResources(); final int[] arrays = getResourceIds(getArrayResources()); final int[] sizes = new int[arrays.length]; final StringBuilder failures = new StringBuilder(); for(int i = 0; i < arrays.length; i++) { try { sizes[i] = r.getStringArray(arrays[i]).length; } catch (Resources.NotFoundException e) { String name = r.getResourceName(arrays[i]); failures.append(String.format("error opening %s: %s\n", name, e.getMessage())); sizes[i] = -1; } } forEachLocale(new Runnable() { public void run() { for(int i = 0; i < arrays.length; i++) { if(sizes[i] == -1) continue; int size; try { size = r.getStringArray(arrays[i]).length; } catch (Resources.NotFoundException e) { String name = r.getResourceName(arrays[i]); Locale locale = r.getConfiguration().locale; failures.append(String.format("%s: error opening %s: %s\n", locale, name, e.getMessage())); continue; } if(size != sizes[i]) { String name = r.getResourceName(arrays[i]); Locale locale = r.getConfiguration().locale; failures.append(String.format("%s (%s): size %d != %d\n", name, locale.toString(), size, sizes[i])); } } } }); assertEquals(failures.toString(), 0, errorCount(failures)); } // --- helper methods /** * Count newlines */ private int errorCount(StringBuilder failures) { int count = 0; int pos = -1; while(true) { pos = failures.indexOf("\n", pos + 1); if(pos == -1) return count; count++; } } /** * @return an array of all string resource id's */ public int[] getResourceIds(Class<?> resources) throws Exception { Field[] fields = resources.getDeclaredFields(); List<Integer> ids = new ArrayList<Integer>(fields.length); for(int i = 0; i < fields.length; i++) { try { ids.add(fields[i].getInt(null)); } catch (Exception e) { // not a field we care about } } int[] idsAsIntArray = new int[ids.size()]; for(int i = 0; i < ids.size(); i++) idsAsIntArray[i] = ids.get(i); return idsAsIntArray; } }