From aaefd5666112c4a6fff58783c25cf9a75b468edd Mon Sep 17 00:00:00 2001
From: "Tadashi G. Takaoka" <takaoka@google.com>
Date: Thu, 28 Aug 2014 15:14:36 +0900
Subject: [PATCH] Add !string/<resource_name> reference

This CL introduces new text reference notation !string/<resource_name>
to refer a string resource on the fly.

This notation is mainly used to represent action key labels may refer
a string in a system locale in run-time.

This notation is needed to implement Hinglish and Serbian-Latin
keyboards that need to refer its own action key labels.

Bug: 17169632
Bug: 9687668
Change-Id: I042f6bd04714e0e448cd92031730eb9fb422e6d3
---
 .../keyboard/internal/KeyboardTextsSet.java   |  92 +++---
 .../keyboard/internal/KeyboardTextsTable.java | 122 +++++---
 .../latin/utils/SubtypeLocaleUtils.java       |   2 +-
 tests/res/values/donottranslate.xml           |  18 +-
 .../KeyboardLayoutSetActionLabelBase.java     |   6 +-
 .../KeyboardLayoutSetActionLabelKlpTests.java | 102 ++++++-
 .../internal/MoreKeySpecSplitTests.java       | 200 ++----------
 .../MoreKeySpecStringReferenceTests.java      | 284 ++++++++++++++++++
 .../donottranslate-more-keys.xml              |   8 +
 .../res/values/donottranslate-more-keys.xml   |   8 +
 10 files changed, 547 insertions(+), 295 deletions(-)
 create mode 100644 tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecStringReferenceTests.java

diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
index cd6abeed3d..f9297ac27f 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -25,49 +25,42 @@ import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.utils.RunInLocale;
 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 
-import java.util.HashMap;
 import java.util.Locale;
 
+// TODO: Make this an immutable class.
 public final class KeyboardTextsSet {
     public static final String PREFIX_TEXT = "!text/";
+    private static final String PREFIX_RESOURCE = "!string/";
     public static final String SWITCH_TO_ALPHA_KEY_LABEL = "keylabel_to_alpha";
 
     private static final char BACKSLASH = Constants.CODE_BACKSLASH;
-    private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
+    private static final int MAX_REFERENCE_INDIRECTION = 10;
 
+    private Resources mResources;
+    private Locale mResourceLocale;
+    private String mResourcePackageName;
     private String[] mTextsTable;
-    // Resource name to text map.
-    private HashMap<String, String> mResourceNameToTextsMap = new HashMap<>();
 
     public void setLocale(final Locale locale, final Context context) {
-        mTextsTable = KeyboardTextsTable.getTextsTable(locale);
         final Resources res = context.getResources();
-        final int referenceId = context.getApplicationInfo().labelRes;
-        final String resourcePackageName = res.getResourcePackageName(referenceId);
-        final RunInLocale<Void> job = new RunInLocale<Void>() {
-            @Override
-            protected Void job(final Resources resource) {
-                loadStringResourcesInternal(res, RESOURCE_NAMES, resourcePackageName);
-                return null;
-            }
-        };
         // Null means the current system locale.
-        job.runInLocale(res,
-                SubtypeLocaleUtils.NO_LANGUAGE.equals(locale.toString()) ? null : locale);
+        final String resourcePackageName = res.getResourcePackageName(
+                context.getApplicationInfo().labelRes);
+        setLocale(locale, res, resourcePackageName);
     }
 
     @UsedForTesting
-    void loadStringResourcesInternal(final Resources res, final String[] resourceNames,
+    public void setLocale(final Locale locale, final Resources res,
             final String resourcePackageName) {
-        for (final String resName : resourceNames) {
-            final int resId = res.getIdentifier(resName, "string", resourcePackageName);
-            mResourceNameToTextsMap.put(resName, res.getString(resId));
-        }
+        mResources = res;
+        // Null means the current system locale.
+        mResourceLocale = SubtypeLocaleUtils.NO_LANGUAGE.equals(locale.toString()) ? null : locale;
+        mResourcePackageName = resourcePackageName;
+        mTextsTable = KeyboardTextsTable.getTextsTable(locale);
     }
 
     public String getText(final String name) {
-        final String text = mResourceNameToTextsMap.get(name);
-        return (text != null) ? text : KeyboardTextsTable.getText(name, mTextsTable);
+        return KeyboardTextsTable.getText(name, mTextsTable);
     }
 
     private static int searchTextNameEnd(final String text, final int start) {
@@ -93,13 +86,14 @@ public final class KeyboardTextsSet {
         StringBuilder sb;
         do {
             level++;
-            if (level >= MAX_STRING_REFERENCE_INDIRECTION) {
-                throw new RuntimeException("Too many " + PREFIX_TEXT + "name indirection: " + text);
+            if (level >= MAX_REFERENCE_INDIRECTION) {
+                throw new RuntimeException("Too many " + PREFIX_TEXT + " or " + PREFIX_RESOURCE +
+                        " reference indirection: " + text);
             }
 
-            final int prefixLen = PREFIX_TEXT.length();
+            final int prefixLength = PREFIX_TEXT.length();
             final int size = text.length();
-            if (size < prefixLen) {
+            if (size < prefixLength) {
                 break;
             }
 
@@ -110,10 +104,12 @@ public final class KeyboardTextsSet {
                     if (sb == null) {
                         sb = new StringBuilder(text.substring(0, pos));
                     }
-                    final int end = searchTextNameEnd(text, pos + prefixLen);
-                    final String name = text.substring(pos + prefixLen, end);
-                    sb.append(getText(name));
-                    pos = end - 1;
+                    pos = expandReference(text, pos, PREFIX_TEXT, sb);
+                } else if (text.startsWith(PREFIX_RESOURCE, pos)) {
+                    if (sb == null) {
+                        sb = new StringBuilder(text.substring(0, pos));
+                    }
+                    pos = expandReference(text, pos, PREFIX_RESOURCE, sb);
                 } else if (c == BACKSLASH) {
                     if (sb != null) {
                         // Append both escape character and escaped character.
@@ -132,18 +128,24 @@ public final class KeyboardTextsSet {
         return TextUtils.isEmpty(text) ? null : text;
     }
 
-    // These texts' name should be aligned with the @string/<name> in
-    // values*/strings-action-keys.xml.
-    static final String[] RESOURCE_NAMES = {
-        // Labels for action.
-        "label_go_key",
-        "label_send_key",
-        "label_next_key",
-        "label_done_key",
-        "label_search_key",
-        "label_previous_key",
-        // Other labels.
-        "label_pause_key",
-        "label_wait_key",
-    };
+    private int expandReference(final String text, final int pos, final String prefix,
+            final StringBuilder sb) {
+        final int prefixLength = prefix.length();
+        final int end = searchTextNameEnd(text, pos + prefixLength);
+        final String name = text.substring(pos + prefixLength, end);
+        if (prefix.equals(PREFIX_TEXT)) {
+            sb.append(getText(name));
+        } else { // PREFIX_RESOURCE
+            final String resourcePackageName = mResourcePackageName;
+            final RunInLocale<String> getTextJob = new RunInLocale<String>() {
+                @Override
+                protected String job(final Resources res) {
+                    final int resId = res.getIdentifier(name, "string", resourcePackageName);
+                    return res.getString(resId);
+                }
+            };
+            sb.append(getTextJob.runInLocale(mResources, mResourceLocale));
+        }
+        return end - 1;
+    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java
index 3c33320ead..1920113846 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java
@@ -209,48 +209,56 @@ public final class KeyboardTextsTable {
         /* 123: 1 */ "morekeys_less_than",
         /* 124: 1 */ "morekeys_greater_than",
         /* 125: 1 */ "morekeys_exclamation",
-        /* 126: 0 */ "morekeys_currency_generic",
-        /* 127: 0 */ "morekeys_symbols_1",
-        /* 128: 0 */ "morekeys_symbols_2",
-        /* 129: 0 */ "morekeys_symbols_3",
-        /* 130: 0 */ "morekeys_symbols_4",
-        /* 131: 0 */ "morekeys_symbols_5",
-        /* 132: 0 */ "morekeys_symbols_6",
-        /* 133: 0 */ "morekeys_symbols_7",
-        /* 134: 0 */ "morekeys_symbols_8",
-        /* 135: 0 */ "morekeys_symbols_9",
-        /* 136: 0 */ "morekeys_symbols_0",
-        /* 137: 0 */ "morekeys_am_pm",
-        /* 138: 0 */ "keyspec_settings",
-        /* 139: 0 */ "keyspec_shortcut",
-        /* 140: 0 */ "keyspec_action_next",
-        /* 141: 0 */ "keyspec_action_previous",
-        /* 142: 0 */ "keylabel_to_more_symbol",
-        /* 143: 0 */ "keylabel_tablet_to_more_symbol",
-        /* 144: 0 */ "keylabel_to_phone_numeric",
-        /* 145: 0 */ "keylabel_to_phone_symbols",
-        /* 146: 0 */ "keylabel_time_am",
-        /* 147: 0 */ "keylabel_time_pm",
-        /* 148: 0 */ "keyspec_popular_domain",
-        /* 149: 0 */ "morekeys_popular_domain",
-        /* 150: 0 */ "keyspecs_left_parenthesis_more_keys",
-        /* 151: 0 */ "keyspecs_right_parenthesis_more_keys",
-        /* 152: 0 */ "single_laqm_raqm",
-        /* 153: 0 */ "single_raqm_laqm",
-        /* 154: 0 */ "double_laqm_raqm",
-        /* 155: 0 */ "double_raqm_laqm",
-        /* 156: 0 */ "single_lqm_rqm",
-        /* 157: 0 */ "single_9qm_lqm",
-        /* 158: 0 */ "single_9qm_rqm",
-        /* 159: 0 */ "single_rqm_9qm",
-        /* 160: 0 */ "double_lqm_rqm",
-        /* 161: 0 */ "double_9qm_lqm",
-        /* 162: 0 */ "double_9qm_rqm",
-        /* 163: 0 */ "double_rqm_9qm",
-        /* 164: 0 */ "morekeys_single_quote",
-        /* 165: 0 */ "morekeys_double_quote",
-        /* 166: 0 */ "morekeys_tablet_double_quote",
-        /* 167: 0 */ "keyspec_emoji_action_key",
+        /* 126: 1 */ "label_go_key",
+        /* 127: 1 */ "label_send_key",
+        /* 128: 1 */ "label_next_key",
+        /* 129: 1 */ "label_done_key",
+        /* 130: 1 */ "label_search_key",
+        /* 131: 1 */ "label_previous_key",
+        /* 132: 1 */ "label_pause_key",
+        /* 133: 1 */ "label_wait_key",
+        /* 134: 0 */ "morekeys_currency_generic",
+        /* 135: 0 */ "morekeys_symbols_1",
+        /* 136: 0 */ "morekeys_symbols_2",
+        /* 137: 0 */ "morekeys_symbols_3",
+        /* 138: 0 */ "morekeys_symbols_4",
+        /* 139: 0 */ "morekeys_symbols_5",
+        /* 140: 0 */ "morekeys_symbols_6",
+        /* 141: 0 */ "morekeys_symbols_7",
+        /* 142: 0 */ "morekeys_symbols_8",
+        /* 143: 0 */ "morekeys_symbols_9",
+        /* 144: 0 */ "morekeys_symbols_0",
+        /* 145: 0 */ "morekeys_am_pm",
+        /* 146: 0 */ "keyspec_settings",
+        /* 147: 0 */ "keyspec_shortcut",
+        /* 148: 0 */ "keyspec_action_next",
+        /* 149: 0 */ "keyspec_action_previous",
+        /* 150: 0 */ "keylabel_to_more_symbol",
+        /* 151: 0 */ "keylabel_tablet_to_more_symbol",
+        /* 152: 0 */ "keylabel_to_phone_numeric",
+        /* 153: 0 */ "keylabel_to_phone_symbols",
+        /* 154: 0 */ "keylabel_time_am",
+        /* 155: 0 */ "keylabel_time_pm",
+        /* 156: 0 */ "keyspec_popular_domain",
+        /* 157: 0 */ "morekeys_popular_domain",
+        /* 158: 0 */ "keyspecs_left_parenthesis_more_keys",
+        /* 159: 0 */ "keyspecs_right_parenthesis_more_keys",
+        /* 160: 0 */ "single_laqm_raqm",
+        /* 161: 0 */ "single_raqm_laqm",
+        /* 162: 0 */ "double_laqm_raqm",
+        /* 163: 0 */ "double_raqm_laqm",
+        /* 164: 0 */ "single_lqm_rqm",
+        /* 165: 0 */ "single_9qm_lqm",
+        /* 166: 0 */ "single_9qm_rqm",
+        /* 167: 0 */ "single_rqm_9qm",
+        /* 168: 0 */ "double_lqm_rqm",
+        /* 169: 0 */ "double_9qm_lqm",
+        /* 170: 0 */ "double_9qm_rqm",
+        /* 171: 0 */ "double_rqm_9qm",
+        /* 172: 0 */ "morekeys_single_quote",
+        /* 173: 0 */ "morekeys_double_quote",
+        /* 174: 0 */ "morekeys_tablet_double_quote",
+        /* 175: 0 */ "keyspec_emoji_action_key",
     };
 
     private static final String EMPTY = "";
@@ -379,6 +387,14 @@ public final class KeyboardTextsTable {
         /* morekeys_greater_than */ "!fixedColumnOrder!3,!text/keyspec_right_single_angle_quote,!text/keyspec_greater_than_equal,!text/keyspec_right_double_angle_quote",
         // U+00A1: "¡" INVERTED EXCLAMATION MARK
         /* morekeys_exclamation */ "\u00A1",
+        /* label_go_key */ "!string/label_go_key",
+        /* label_send_key */ "!string/label_send_key",
+        /* label_next_key */ "!string/label_next_key",
+        /* label_done_key */ "!string/label_done_key",
+        /* label_search_key */ "!string/label_search_key",
+        /* label_previous_key */ "!string/label_previous_key",
+        /* label_pause_key */ "!string/label_pause_key",
+        /* label_wait_key */ "!string/label_wait_key",
         /* morekeys_currency_generic */ "$,\u00A2,\u20AC,\u00A3,\u00A5,\u20B1",
         // U+00B9: "¹" SUPERSCRIPT ONE
         // U+00BD: "½" VULGAR FRACTION ONE HALF
@@ -1885,6 +1901,24 @@ public final class KeyboardTextsTable {
         /* ~ morekeys_s */
         // U+20B9: "₹" INDIAN RUPEE SIGN
         /* keyspec_currency */ "\u20B9",
+        /* morekeys_y ~ */
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        null, null, null, null, null, null, null, null, null,
+        /* ~ morekeys_exclamation */
+        /* label_go_key */ "Go",
+        /* label_send_key */ "Send",
+        /* label_next_key */ "Next",
+        /* label_done_key */ "Done",
+        /* label_search_key */ "Search",
+        /* label_previous_key */ "Prev",
+        /* label_pause_key */ "Pause",
+        /* label_wait_key */ "Wait",
     };
 
     /* Locale hr: Croatian */
@@ -3952,7 +3986,7 @@ public final class KeyboardTextsTable {
 
     private static final Object[] LOCALES_AND_TEXTS = {
     // "locale", TEXT_ARRAY,  /* numberOfNonNullText/lengthOf_TEXT_ARRAY localeName */
-        "DEFAULT", TEXTS_DEFAULT, /* 168/168 DEFAULT */
+        "DEFAULT", TEXTS_DEFAULT, /* 176/176 DEFAULT */
         "af"     , TEXTS_af,    /*   7/ 13 Afrikaans */
         "ar"     , TEXTS_ar,    /*  55/110 Arabic */
         "az_AZ"  , TEXTS_az_AZ, /*  11/ 18 Azerbaijani (Azerbaijan) */
@@ -3974,7 +4008,7 @@ public final class KeyboardTextsTable {
         "fr"     , TEXTS_fr,    /*  13/ 62 French */
         "gl_ES"  , TEXTS_gl_ES, /*   7/  8 Gallegan (Spain) */
         "hi"     , TEXTS_hi,    /*  23/ 53 Hindi */
-        "hi_ZZ"  , TEXTS_hi_ZZ, /*   1/ 12 Hindi (ZZ) */
+        "hi_ZZ"  , TEXTS_hi_ZZ, /*   9/134 Hindi (ZZ) */
         "hr"     , TEXTS_hr,    /*   9/ 20 Croatian */
         "hu"     , TEXTS_hu,    /*   9/ 20 Hungarian */
         "hy_AM"  , TEXTS_hy_AM, /*   9/126 Armenian (Armenia) */
diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
index 96a6510fc9..5a7f7662c8 100644
--- a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
@@ -167,7 +167,7 @@ public final class SubtypeLocaleUtils {
         return nameId == null ? UNKNOWN_KEYBOARD_LAYOUT : nameId;
     }
 
-    private static Locale getDisplayLocaleOfSubtypeLocale(final String localeString) {
+    public static Locale getDisplayLocaleOfSubtypeLocale(final String localeString) {
         if (NO_LANGUAGE.equals(localeString)) {
             return sResources.getConfiguration().locale;
         }
diff --git a/tests/res/values/donottranslate.xml b/tests/res/values/donottranslate.xml
index 263d0af243..f269476085 100644
--- a/tests/res/values/donottranslate.xml
+++ b/tests/res/values/donottranslate.xml
@@ -50,13 +50,15 @@
     <string name="multiple_labels_with_escape_surrounded_by_spaces">" \\abc , d\\ef , gh\\i "</string>
     <string name="multiple_labels_with_comma_and_escape">"ab\\\\,d\\\\\\,,g\\,i"</string>
     <string name="multiple_labels_with_comma_and_escape_surrounded_by_spaces">" ab\\\\ , d\\\\\\, , g\\,i "</string>
-    <string name="indirect_string">!text/multiple_chars</string>
-    <string name="indirect_string_with_literal">x,!text/multiple_chars,y</string>
-    <string name="indirect2_string">!text/indirect_string</string>
-    <string name="infinite_indirection">infinite,!text/infinite_indirection,loop</string>
-    <string name="upper_indirect_string">!TEXT/MULTIPLE_CHARS</string>
-    <string name="upper_indirect_string_with_literal">x,!TEXT/MULTIPLE_CHARS,y</string>
-    <string name="upper_indirect2_string">!TEXT/UPPER_INDIRECT_STRING</string>
-    <string name="upper_infinite_indirection">infinite,!TEXT/INFINITE_INDIRECTION,loop</string>
+    <string name="indirect_string">!string/multiple_chars</string>
+    <string name="indirect_string_with_literal">x,!string/multiple_chars,y</string>
+    <string name="indirect2_string">!string/indirect_string</string>
+    <string name="infinite_indirection">infinite,!string/infinite_indirection,loop</string>
+    <string name="upper_indirect_string">!STRING/MULTIPLE_CHARS</string>
+    <string name="upper_indirect_string_with_literal">x,!STRING/MULTIPLE_CHARS,y</string>
+    <string name="upper_indirect2_string">!STRING/UPPER_INDIRECT_STRING</string>
+    <string name="upper_infinite_indirection">infinite,!STRING/INFINITE_INDIRECTION,loop</string>
     <string name="keyspec_indirect_navigate_actions">!fixedColumnOrder!2,!text/keyspec_action_previous,!text/keyspec_action_next</string>
+    <string name="label_next_key">ActionNext</string>
+    <string name="label_previous_key">ActionPrevious</string>
 </resources>
diff --git a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetActionLabelBase.java b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetActionLabelBase.java
index 17cee29b34..22c22a00aa 100644
--- a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetActionLabelBase.java
+++ b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetActionLabelBase.java
@@ -25,6 +25,7 @@ import android.view.inputmethod.InputMethodSubtype;
 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
 import com.android.inputmethod.keyboard.layout.expected.ExpectedKeyVisual;
 import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.utils.LocaleUtils;
 import com.android.inputmethod.latin.utils.RunInLocale;
 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 
@@ -64,10 +65,11 @@ abstract class KeyboardLayoutSetActionLabelBase extends KeyboardLayoutSetTestsBa
     }
 
     protected static Locale getLabelLocale(final InputMethodSubtype subtype) {
-        if (subtype.getLocale().equals(SubtypeLocaleUtils.NO_LANGUAGE)) {
+        final String localeString = subtype.getLocale();
+        if (localeString.equals(SubtypeLocaleUtils.NO_LANGUAGE)) {
             return null;
         }
-        return SubtypeLocaleUtils.getSubtypeLocale(subtype);
+        return LocaleUtils.constructLocaleFromString(localeString);
     }
 
     public void testActionUnspecified() {
diff --git a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetActionLabelKlpTests.java b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetActionLabelKlpTests.java
index 07d47053a8..36b9c9cfc2 100644
--- a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetActionLabelKlpTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetActionLabelKlpTests.java
@@ -22,15 +22,40 @@ import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
+import com.android.inputmethod.keyboard.internal.KeyboardTextsSet;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.RichInputMethodManager;
 import com.android.inputmethod.latin.utils.RunInLocale;
 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 
+import java.util.ArrayList;
 import java.util.Locale;
 
 @MediumTest
 public class KeyboardLayoutSetActionLabelKlpTests extends KeyboardLayoutSetActionLabelBase {
+    // Filter a subtype whose name should be displayed using {@link Locale#ROOT}, such like
+    // Hinglish (hi_ZZ) and Serbian-Latn (sr_ZZ).
+    static final SubtypeFilter SUBTYPE_FILTER_NAME_IN_BASE_LOCALE = new SubtypeFilter() {
+        @Override
+        public boolean accept(final InputMethodSubtype subtype) {
+            return Locale.ROOT.equals(
+                    SubtypeLocaleUtils.getDisplayLocaleOfSubtypeLocale(subtype.getLocale()));
+        }
+    };
+
+    private ArrayList<InputMethodSubtype> mSubtypesWhoseNameIsDisplayedInItsLocale;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mSubtypesWhoseNameIsDisplayedInItsLocale = getSubtypesFilteredBy(new SubtypeFilter() {
+            @Override
+            public boolean accept(final InputMethodSubtype subtype) {
+                return !SUBTYPE_FILTER_NAME_IN_BASE_LOCALE.accept(subtype);
+            }
+        });
+    }
+
     @Override
     protected int getKeyboardThemeForTests() {
         return KeyboardTheme.THEME_ID_KLP;
@@ -38,7 +63,7 @@ public class KeyboardLayoutSetActionLabelKlpTests extends KeyboardLayoutSetActio
 
     @Override
     public void testActionGo() {
-        for (final InputMethodSubtype subtype : getAllSubtypesList()) {
+        for (final InputMethodSubtype subtype : mSubtypesWhoseNameIsDisplayedInItsLocale) {
             final String tag = "go " + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
             final ExpectedActionKey expectedKey = ExpectedActionKey.newLabelKey(
                     R.string.label_go_key, getLabelLocale(subtype), getContext());
@@ -48,7 +73,7 @@ public class KeyboardLayoutSetActionLabelKlpTests extends KeyboardLayoutSetActio
 
     @Override
     public void testActionSend() {
-        for (final InputMethodSubtype subtype : getAllSubtypesList()) {
+        for (final InputMethodSubtype subtype : mSubtypesWhoseNameIsDisplayedInItsLocale) {
             final String tag = "send " + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
             final ExpectedActionKey expectedKey = ExpectedActionKey.newLabelKey(
                     R.string.label_send_key, getLabelLocale(subtype), getContext());
@@ -58,7 +83,7 @@ public class KeyboardLayoutSetActionLabelKlpTests extends KeyboardLayoutSetActio
 
     @Override
     public void testActionNext() {
-        for (final InputMethodSubtype subtype : getAllSubtypesList()) {
+        for (final InputMethodSubtype subtype : mSubtypesWhoseNameIsDisplayedInItsLocale) {
             final String tag = "next " + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
             final ExpectedActionKey expectedKey = ExpectedActionKey.newLabelKey(
                     R.string.label_next_key, getLabelLocale(subtype), getContext());
@@ -68,7 +93,7 @@ public class KeyboardLayoutSetActionLabelKlpTests extends KeyboardLayoutSetActio
 
     @Override
     public void testActionDone() {
-        for (final InputMethodSubtype subtype : getAllSubtypesList()) {
+        for (final InputMethodSubtype subtype : mSubtypesWhoseNameIsDisplayedInItsLocale) {
             final String tag = "done " + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
             final ExpectedActionKey expectedKey = ExpectedActionKey.newLabelKey(
                     R.string.label_done_key, getLabelLocale(subtype), getContext());
@@ -78,7 +103,7 @@ public class KeyboardLayoutSetActionLabelKlpTests extends KeyboardLayoutSetActio
 
     @Override
     public void testActionPrevious() {
-        for (final InputMethodSubtype subtype : getAllSubtypesList()) {
+        for (final InputMethodSubtype subtype : mSubtypesWhoseNameIsDisplayedInItsLocale) {
             final String tag = "previous " + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
             final ExpectedActionKey expectedKey = ExpectedActionKey.newLabelKey(
                     R.string.label_previous_key, getLabelLocale(subtype), getContext());
@@ -105,7 +130,7 @@ public class KeyboardLayoutSetActionLabelKlpTests extends KeyboardLayoutSetActio
     // Working variable to simulate system locale changing.
     private Locale mSystemLocale = Locale.getDefault();
 
-    private void doTestActionKeysInLocale(final InputMethodSubtype subtype,
+    private void doTestActionKeysInLocaleWithStringResources(final InputMethodSubtype subtype,
             final Locale labelLocale, final Locale systemLocale) {
         // Simulate system locale changing, see {@link SystemBroadcastReceiver}.
         if (!systemLocale.equals(mSystemLocale)) {
@@ -144,10 +169,10 @@ public class KeyboardLayoutSetActionLabelKlpTests extends KeyboardLayoutSetActio
         final InputMethodSubtype italian = richImm.findSubtypeByLocaleAndKeyboardLayoutSet(
                 Locale.ITALIAN.toString(), SubtypeLocaleUtils.QWERTY);
         // An action label should be displayed in subtype's locale regardless of the system locale.
-        doTestActionKeysInLocale(italian, Locale.ITALIAN, Locale.US);
-        doTestActionKeysInLocale(italian, Locale.ITALIAN, Locale.FRENCH);
-        doTestActionKeysInLocale(italian, Locale.ITALIAN, Locale.ITALIAN);
-        doTestActionKeysInLocale(italian, Locale.ITALIAN, Locale.JAPANESE);
+        doTestActionKeysInLocaleWithStringResources(italian, Locale.ITALIAN, Locale.US);
+        doTestActionKeysInLocaleWithStringResources(italian, Locale.ITALIAN, Locale.FRENCH);
+        doTestActionKeysInLocaleWithStringResources(italian, Locale.ITALIAN, Locale.ITALIAN);
+        doTestActionKeysInLocaleWithStringResources(italian, Locale.ITALIAN, Locale.JAPANESE);
     }
 
     public void testNoLanguageSubtypeActionLabel() {
@@ -155,9 +180,58 @@ public class KeyboardLayoutSetActionLabelKlpTests extends KeyboardLayoutSetActio
         final InputMethodSubtype noLanguage = richImm.findSubtypeByLocaleAndKeyboardLayoutSet(
                 SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY);
         // An action label of no language keyboard should be displayed in the system locale.
-        doTestActionKeysInLocale(noLanguage, Locale.US, Locale.US);
-        doTestActionKeysInLocale(noLanguage, Locale.FRENCH, Locale.FRENCH);
-        doTestActionKeysInLocale(noLanguage, Locale.ITALIAN, Locale.ITALIAN);
-        doTestActionKeysInLocale(noLanguage, Locale.JAPANESE, Locale.JAPANESE);
+        doTestActionKeysInLocaleWithStringResources(noLanguage, Locale.US, Locale.US);
+        doTestActionKeysInLocaleWithStringResources(noLanguage, Locale.FRENCH, Locale.FRENCH);
+        doTestActionKeysInLocaleWithStringResources(noLanguage, Locale.ITALIAN, Locale.ITALIAN);
+        doTestActionKeysInLocaleWithStringResources(noLanguage, Locale.JAPANESE, Locale.JAPANESE);
+    }
+
+    private void doTestActionKeysInLocaleWithKeyboardTextsSet(final InputMethodSubtype subtype,
+            final Locale labelLocale, final Locale systemLocale) {
+        // Simulate system locale changing, see {@link SystemBroadcastReceiver}.
+        if (!systemLocale.equals(mSystemLocale)) {
+            KeyboardLayoutSet.onSystemLocaleChanged();
+            mSystemLocale = systemLocale;
+        }
+        final KeyboardTextsSet textsSet = new KeyboardTextsSet();
+        textsSet.setLocale(labelLocale, getContext());
+        final ExpectedActionKey enterKey = ExpectedActionKey.newIconKey(
+                KeyboardIconsSet.NAME_ENTER_KEY);
+        final ExpectedActionKey goKey = ExpectedActionKey.newLabelKey(
+                textsSet.getText("label_go_key"));
+        final ExpectedActionKey searchKey = ExpectedActionKey.newIconKey(
+                KeyboardIconsSet.NAME_SEARCH_KEY);
+        final ExpectedActionKey sendKey = ExpectedActionKey.newLabelKey(
+                textsSet.getText("label_send_key"));
+        final ExpectedActionKey nextKey = ExpectedActionKey.newLabelKey(
+                textsSet.getText("label_next_key"));
+        final ExpectedActionKey doneKey = ExpectedActionKey.newLabelKey(
+                textsSet.getText("label_done_key"));
+        final ExpectedActionKey previousKey = ExpectedActionKey.newLabelKey(
+                textsSet.getText("label_previous_key"));
+        final String tag = "label=hi_ZZ system=" + systemLocale
+                + " " + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
+        final RunInLocale<Void> job = new RunInLocale<Void>() {
+            @Override
+            public Void job(final Resources res) {
+                doTestActionKeys(subtype, tag, enterKey, enterKey, goKey, searchKey, sendKey,
+                        nextKey, doneKey, previousKey);
+                return null;
+            }
+        };
+        job.runInLocale(getContext().getResources(), systemLocale);
+    }
+
+    public void testHinglishActionLabel() {
+        final RichInputMethodManager richImm = RichInputMethodManager.getInstance();
+        final Locale hi_ZZ = new Locale("hi", "ZZ");
+        final InputMethodSubtype hinglish = richImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                hi_ZZ.toString(), SubtypeLocaleUtils.QWERTY);
+        // An action label should be displayed in subtype's locale regardless of the system locale.
+        doTestActionKeysInLocaleWithKeyboardTextsSet(hinglish, hi_ZZ, hi_ZZ);
+        doTestActionKeysInLocaleWithKeyboardTextsSet(hinglish, hi_ZZ, Locale.US);
+        doTestActionKeysInLocaleWithKeyboardTextsSet(hinglish, hi_ZZ, Locale.FRENCH);
+        doTestActionKeysInLocaleWithKeyboardTextsSet(hinglish, hi_ZZ, Locale.ITALIAN);
+        doTestActionKeysInLocaleWithKeyboardTextsSet(hinglish, hi_ZZ, Locale.JAPANESE);
     }
 }
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
index 922d2a8c80..8f4648c52d 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
@@ -16,52 +16,33 @@
 
 package com.android.inputmethod.keyboard.internal;
 
-import android.app.Instrumentation;
 import android.content.Context;
 import android.content.res.Resources;
-import android.test.InstrumentationTestCase;
-import android.test.suitebuilder.annotation.MediumTest;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.inputmethod.latin.R;
 
-import java.lang.reflect.Field;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Locale;
 
-@MediumTest
-public class MoreKeySpecSplitTests extends InstrumentationTestCase {
+@SmallTest
+public class MoreKeySpecSplitTests extends AndroidTestCase {
     private static final Locale TEST_LOCALE = Locale.ENGLISH;
-    final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
+    private final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
 
-        final Instrumentation instrumentation = getInstrumentation();
-        final Context targetContext = instrumentation.getTargetContext();
-        mTextsSet.setLocale(TEST_LOCALE, targetContext);
-        final String[] testResourceNames = getAllResourceIdNames(
-                com.android.inputmethod.latin.tests.R.string.class);
-        final Context testContext = instrumentation.getContext();
-        final Resources testRes = testContext.getResources();
-        final String testResPackageName = testRes.getResourcePackageName(
-                // This dummy raw resource is needed to be able to load string resources from a test
-                // APK successfully.
-                com.android.inputmethod.latin.tests.R.raw.dummy_resource_for_testing);
-        mTextsSet.loadStringResourcesInternal(testRes, testResourceNames, testResPackageName);
-    }
-
-    private static String[] getAllResourceIdNames(final Class<?> resourceIdClass) {
-        final ArrayList<String> names = new ArrayList<>();
-        for (final Field field : resourceIdClass.getFields()) {
-            if (field.getType() == int.class) {
-                names.add(field.getName());
-            }
-        }
-        return names.toArray(new String[names.size()]);
+        final Context targetContext = getContext();
+        final Resources targetRes = targetContext.getResources();
+        final String targetPackageName = targetRes.getResourcePackageName(
+                R.string.english_ime_name);
+        mTextsSet.setLocale(TEST_LOCALE, targetRes, targetPackageName);
     }
 
-    private static <T> void assertArrayEquals(final String message, final T[] expected,
-            final T[] actual) {
+    static <T> void assertArrayEquals(final String message, final T[] expected, final T[] actual) {
         if (expected == actual) {
             return;
         }
@@ -109,14 +90,6 @@ public class MoreKeySpecSplitTests extends InstrumentationTestCase {
     private static final String SURROGATE1 = PAIR1 + PAIR2;
     private static final String SURROGATE2 = PAIR1 + PAIR2 + PAIR3;
 
-    public void testResolveNullText() {
-        assertNull("resolve null", mTextsSet.resolveTextReference(null));
-    }
-
-    public void testResolveEmptyText() {
-        assertNull("resolve empty text", mTextsSet.resolveTextReference("!text/empty_string"));
-    }
-
     public void testSplitZero() {
         assertTextArray("Empty string", "");
         assertTextArray("Empty entry", ",");
@@ -224,132 +197,14 @@ public class MoreKeySpecSplitTests extends InstrumentationTestCase {
                 "\\!", "\\!TEXT/EMPTY_STRING");
     }
 
-    public void testSplitResourceError() {
-        assertError("Incomplete resource name", "!text/", "!text/");
-        assertError("Non existing resource", "!text/non_existing");
-    }
-
-    public void testSplitResourceZero() {
-        assertTextArray("Empty string",
-                "!text/empty_string");
-    }
-
-    public void testSplitResourceSingle() {
-        assertTextArray("Single char",
-                "!text/single_char", "a");
-        assertTextArray("Space",
-                "!text/space", " ");
-        assertTextArray("Single label",
-                "!text/single_label", "abc");
-        assertTextArray("Spaces",
-                "!text/spaces", "   ");
-        assertTextArray("Spaces in label",
-                "!text/spaces_in_label", "a b c");
-        assertTextArray("Spaces at beginning of label",
-                "!text/spaces_at_beginning_of_label", " abc");
-        assertTextArray("Spaces at end of label",
-                "!text/spaces_at_end_of_label", "abc ");
-        assertTextArray("label surrounded by spaces",
-                "!text/label_surrounded_by_spaces", " abc ");
-
-        assertTextArray("Escape and single char",
-                "\\\\!text/single_char", "\\\\a");
-    }
-
-    public void testSplitResourceSingleEscaped() {
-        assertTextArray("Escaped char",
-                "!text/escaped_char", "\\a");
-        assertTextArray("Escaped comma",
-                "!text/escaped_comma", "\\,");
-        assertTextArray("Escaped comma escape",
-                "!text/escaped_comma_escape", "a\\,\\");
-        assertTextArray("Escaped escape",
-                "!text/escaped_escape", "\\\\");
-        assertTextArray("Escaped label",
-                "!text/escaped_label", "a\\bc");
-        assertTextArray("Escaped label at beginning",
-                "!text/escaped_label_at_beginning", "\\abc");
-        assertTextArray("Escaped label at end",
-                "!text/escaped_label_at_end", "abc\\");
-        assertTextArray("Escaped label with comma",
-                "!text/escaped_label_with_comma", "a\\,c");
-        assertTextArray("Escaped label with comma at beginning",
-                "!text/escaped_label_with_comma_at_beginning", "\\,bc");
-        assertTextArray("Escaped label with comma at end",
-                "!text/escaped_label_with_comma_at_end", "ab\\,");
-        assertTextArray("Escaped label with successive",
-                "!text/escaped_label_with_successive", "\\,\\\\bc");
-        assertTextArray("Escaped label with escape",
-                "!text/escaped_label_with_escape", "a\\\\c");
+    public void testSplitTextReferenceError() {
+        assertError("Incomplete text name", "!text/", "!text/");
+        assertError("Non existing text", "!text/non_existing");
     }
 
-    public void testSplitResourceMulti() {
-        assertTextArray("Multiple chars",
-                "!text/multiple_chars", "a", "b", "c");
-        assertTextArray("Multiple chars surrounded by spaces",
-                "!text/multiple_chars_surrounded_by_spaces",
-                " a ", " b ", " c ");
-        assertTextArray("Multiple labels",
-                "!text/multiple_labels", "abc", "def", "ghi");
-        assertTextArray("Multiple labels surrounded by spaces",
-                "!text/multiple_labels_surrounded_by_spaces", " abc ", " def ", " ghi ");
-    }
-
-    public void testSplitResourcetMultiEscaped() {
-        assertTextArray("Multiple chars with comma",
-                "!text/multiple_chars_with_comma",
-                "a", "\\,", "c");
-        assertTextArray("Multiple chars with comma surrounded by spaces",
-                "!text/multiple_chars_with_comma_surrounded_by_spaces",
-                " a ", " \\, ", " c ");
-        assertTextArray("Multiple labels with escape",
-                "!text/multiple_labels_with_escape",
-                "\\abc", "d\\ef", "gh\\i");
-        assertTextArray("Multiple labels with escape surrounded by spaces",
-                "!text/multiple_labels_with_escape_surrounded_by_spaces",
-                " \\abc ", " d\\ef ", " gh\\i ");
-        assertTextArray("Multiple labels with comma and escape",
-                "!text/multiple_labels_with_comma_and_escape",
-                "ab\\\\", "d\\\\\\,", "g\\,i");
-        assertTextArray("Multiple labels with comma and escape surrounded by spaces",
-                "!text/multiple_labels_with_comma_and_escape_surrounded_by_spaces",
-                " ab\\\\ ", " d\\\\\\, ", " g\\,i ");
-    }
-
-    public void testSplitMultipleResources() {
-        assertTextArray("Literals and resources",
-                "1,!text/multiple_chars,z", "1", "a", "b", "c", "z");
-        assertTextArray("Literals and resources and escape at end",
-                "\\1,!text/multiple_chars,z\\", "\\1", "a", "b", "c", "z\\");
-        assertTextArray("Multiple single resource chars and labels",
-                "!text/single_char,!text/single_label,!text/escaped_comma",
-                "a", "abc", "\\,");
-        assertTextArray("Multiple single resource chars and labels 2",
-                "!text/single_char,!text/single_label,!text/escaped_comma_escape",
-                "a", "abc", "a\\,\\");
-        assertTextArray("Multiple multiple resource chars and labels",
-                "!text/multiple_chars,!text/multiple_labels,!text/multiple_chars_with_comma",
-                "a", "b", "c", "abc", "def", "ghi", "a", "\\,", "c");
-        assertTextArray("Concatenated resources",
-                "!text/multiple_chars!text/multiple_labels!text/multiple_chars_with_comma",
-                "a", "b", "cabc", "def", "ghia", "\\,", "c");
-        assertTextArray("Concatenated resource and literal",
-                "abc!text/multiple_labels",
-                "abcabc", "def", "ghi");
-    }
-
-    public void testSplitIndirectReference() {
-        assertTextArray("Indirect",
-                "!text/indirect_string", "a", "b", "c");
-        assertTextArray("Indirect with literal",
-                "1,!text/indirect_string_with_literal,2", "1", "x", "a", "b", "c", "y", "2");
-        assertTextArray("Indirect2",
-                "!text/indirect2_string", "a", "b", "c");
-    }
-
-    public void testSplitInfiniteIndirectReference() {
-        assertError("Infinite indirection",
-                "1,!text/infinite_indirection,2", "1", "infinite", "<infinite>", "loop", "2");
+    public void testSplitEmptyTextReference() {
+        // Note that morekeys_q of English locale is empty.
+        assertTextArray("Empty string", "!text/morekeys_q");
     }
 
     public void testLabelReferece() {
@@ -360,12 +215,6 @@ public class MoreKeySpecSplitTests extends InstrumentationTestCase {
 
         assertTextArray("Settings as more key", "!text/keyspec_settings",
                 "!icon/settings_key|!code/key_settings");
-
-        assertTextArray("Indirect naviagte actions as more key",
-                "!text/keyspec_indirect_navigate_actions",
-                "!fixedColumnOrder!2",
-                "!hasLabels!", "Prev|!code/key_action_previous",
-                "!hasLabels!", "Next|!code/key_action_next");
     }
 
     public void testUselessUpperCaseSpecifier() {
@@ -394,14 +243,6 @@ public class MoreKeySpecSplitTests extends InstrumentationTestCase {
         assertTextArray("INDIRECT2",
                 "!TEXT/INDIRECT2_STRING", "!TEXT/INDIRECT2_STRING");
 
-        assertTextArray("Upper indirect",
-                "!text/upper_indirect_string", "!TEXT/MULTIPLE_CHARS");
-        assertTextArray("Upper indirect with literal",
-                "1,!text/upper_indirect_string_with_literal,2",
-                "1", "x", "!TEXT/MULTIPLE_CHARS", "y", "2");
-        assertTextArray("Upper indirect2",
-                "!text/upper_indirect2_string", "!TEXT/UPPER_INDIRECT_STRING");
-
         assertTextArray("UPPER INDIRECT",
                 "!TEXT/upper_INDIRECT_STRING", "!TEXT/upper_INDIRECT_STRING");
         assertTextArray("Upper INDIRECT with literal",
@@ -413,9 +254,6 @@ public class MoreKeySpecSplitTests extends InstrumentationTestCase {
         assertTextArray("INFINITE INDIRECTION",
                 "1,!TEXT/INFINITE_INDIRECTION,2", "1", "!TEXT/INFINITE_INDIRECTION", "2");
 
-        assertTextArray("Upper infinite indirection",
-                "1,!text/upper_infinite_indirection,2",
-                "1", "infinite", "!TEXT/INFINITE_INDIRECTION", "loop", "2");
         assertTextArray("Upper INFINITE INDIRECTION",
                 "1,!TEXT/UPPER_INFINITE_INDIRECTION,2",
                 "1", "!TEXT/UPPER_INFINITE_INDIRECTION", "2");
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecStringReferenceTests.java b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecStringReferenceTests.java
new file mode 100644
index 0000000000..e06ecaedfc
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecStringReferenceTests.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.res.Resources;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.inputmethod.latin.tests.R;
+
+import java.util.Locale;
+
+@SmallTest
+public class MoreKeySpecStringReferenceTests extends InstrumentationTestCase {
+    private static final Locale TEST_LOCALE = Locale.ENGLISH;
+    private final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        final Instrumentation instrumentation = getInstrumentation();
+        final Context testContext = instrumentation.getContext();
+        final Resources testRes = testContext.getResources();
+        final String testPackageName = testRes.getResourcePackageName(R.string.empty_string);
+        mTextsSet.setLocale(TEST_LOCALE, testRes, testPackageName);
+    }
+
+    private void assertTextArray(final String message, final String value,
+            final String ... expectedArray) {
+        final String resolvedActual = mTextsSet.resolveTextReference(value);
+        final String[] actual = MoreKeySpec.splitKeySpecs(resolvedActual);
+        final String[] expected = (expectedArray.length == 0) ? null : expectedArray;
+        MoreKeySpecSplitTests.assertArrayEquals(message, expected, actual);
+    }
+
+    private void assertError(final String message, final String value, final String ... expected) {
+        try {
+            assertTextArray(message, value, expected);
+            fail(message);
+        } catch (Exception pcpe) {
+            // success.
+        }
+    }
+
+    public void testResolveNullText() {
+        assertEquals("resolve null",
+                mTextsSet.resolveTextReference(null), null);
+    }
+
+    public void testResolveEmptyText() {
+        assertEquals("resolve empty text",
+                mTextsSet.resolveTextReference("!string/empty_string"), null);
+    }
+
+    public void testSplitSingleEscaped() {
+        assertTextArray("Escaped !string", "\\!string",
+                "\\!string");
+        assertTextArray("Escaped !string/", "\\!string/",
+                "\\!string/");
+        assertTextArray("Escaped !STRING/", "\\!STRING/",
+                "\\!STRING/");
+        assertTextArray("Escaped !string/name", "\\!string/empty_string",
+                "\\!string/empty_string");
+        assertTextArray("Escaped !STRING/NAME", "\\!STRING/EMPTY_STRING",
+                "\\!STRING/EMPTY_STRING");
+    }
+
+    public void testSplitMultiEscaped() {
+        assertTextArray("Multiple escaped !string", "\\!,\\!string/empty_string",
+                "\\!", "\\!string/empty_string");
+        assertTextArray("Multiple escaped !STRING", "\\!,\\!STRING/EMPTY_STRING",
+                "\\!", "\\!STRING/EMPTY_STRING");
+    }
+
+    public void testSplitStringReferenceError() {
+        assertError("Incomplete resource name", "!string/", "!string/");
+        assertError("Non existing resource", "!string/non_existing");
+    }
+
+    public void testSplitEmptyStringReference() {
+        assertTextArray("Empty string", "!string/empty_string");
+    }
+
+    public void testSplitResourceSingle() {
+        assertTextArray("Single char", "!string/single_char",
+                "a");
+        assertTextArray("Space", "!string/space",
+                " ");
+        assertTextArray("Single label", "!string/single_label",
+                "abc");
+        assertTextArray("Spaces", "!string/spaces",
+                "   ");
+        assertTextArray("Spaces in label", "!string/spaces_in_label",
+                "a b c");
+        assertTextArray("Spaces at beginning of label", "!string/spaces_at_beginning_of_label",
+                " abc");
+        assertTextArray("Spaces at end of label", "!string/spaces_at_end_of_label",
+                "abc ");
+        assertTextArray("label surrounded by spaces", "!string/label_surrounded_by_spaces",
+                " abc ");
+        assertTextArray("Escape and single char", "\\\\!string/single_char",
+                "\\\\a");
+    }
+
+    public void testSplitResourceSingleEscaped() {
+        assertTextArray("Escaped char",
+                "!string/escaped_char", "\\a");
+        assertTextArray("Escaped comma",
+                "!string/escaped_comma", "\\,");
+        assertTextArray("Escaped comma escape",
+                "!string/escaped_comma_escape", "a\\,\\");
+        assertTextArray("Escaped escape",
+                "!string/escaped_escape", "\\\\");
+        assertTextArray("Escaped label",
+                "!string/escaped_label", "a\\bc");
+        assertTextArray("Escaped label at beginning",
+                "!string/escaped_label_at_beginning", "\\abc");
+        assertTextArray("Escaped label at end",
+                "!string/escaped_label_at_end", "abc\\");
+        assertTextArray("Escaped label with comma",
+                "!string/escaped_label_with_comma", "a\\,c");
+        assertTextArray("Escaped label with comma at beginning",
+                "!string/escaped_label_with_comma_at_beginning", "\\,bc");
+        assertTextArray("Escaped label with comma at end",
+                "!string/escaped_label_with_comma_at_end", "ab\\,");
+        assertTextArray("Escaped label with successive",
+                "!string/escaped_label_with_successive", "\\,\\\\bc");
+        assertTextArray("Escaped label with escape",
+                "!string/escaped_label_with_escape", "a\\\\c");
+    }
+
+    public void testSplitResourceMulti() {
+        assertTextArray("Multiple chars",
+                "!string/multiple_chars", "a", "b", "c");
+        assertTextArray("Multiple chars surrounded by spaces",
+                "!string/multiple_chars_surrounded_by_spaces",
+                " a ", " b ", " c ");
+        assertTextArray("Multiple labels",
+                "!string/multiple_labels", "abc", "def", "ghi");
+        assertTextArray("Multiple labels surrounded by spaces",
+                "!string/multiple_labels_surrounded_by_spaces", " abc ", " def ", " ghi ");
+    }
+
+    public void testSplitResourcetMultiEscaped() {
+        assertTextArray("Multiple chars with comma",
+                "!string/multiple_chars_with_comma",
+                "a", "\\,", "c");
+        assertTextArray("Multiple chars with comma surrounded by spaces",
+                "!string/multiple_chars_with_comma_surrounded_by_spaces",
+                " a ", " \\, ", " c ");
+        assertTextArray("Multiple labels with escape",
+                "!string/multiple_labels_with_escape",
+                "\\abc", "d\\ef", "gh\\i");
+        assertTextArray("Multiple labels with escape surrounded by spaces",
+                "!string/multiple_labels_with_escape_surrounded_by_spaces",
+                " \\abc ", " d\\ef ", " gh\\i ");
+        assertTextArray("Multiple labels with comma and escape",
+                "!string/multiple_labels_with_comma_and_escape",
+                "ab\\\\", "d\\\\\\,", "g\\,i");
+        assertTextArray("Multiple labels with comma and escape surrounded by spaces",
+                "!string/multiple_labels_with_comma_and_escape_surrounded_by_spaces",
+                " ab\\\\ ", " d\\\\\\, ", " g\\,i ");
+    }
+
+    public void testSplitMultipleResources() {
+        assertTextArray("Literals and resources",
+                "1,!string/multiple_chars,z",
+                "1", "a", "b", "c", "z");
+        assertTextArray("Literals and resources and escape at end",
+                "\\1,!string/multiple_chars,z\\",
+                "\\1", "a", "b", "c", "z\\");
+        assertTextArray("Multiple single resource chars and labels",
+                "!string/single_char,!string/single_label,!string/escaped_comma",
+                "a", "abc", "\\,");
+        assertTextArray("Multiple single resource chars and labels 2",
+                "!string/single_char,!string/single_label,!string/escaped_comma_escape",
+                "a", "abc", "a\\,\\");
+        assertTextArray("Multiple multiple resource chars and labels",
+                "!string/multiple_chars,!string/multiple_labels,!string/multiple_chars_with_comma",
+                "a", "b", "c", "abc", "def", "ghi", "a", "\\,", "c");
+        assertTextArray("Concatenated resources",
+                "!string/multiple_chars!string/multiple_labels!string/multiple_chars_with_comma",
+                "a", "b", "cabc", "def", "ghia", "\\,", "c");
+        assertTextArray("Concatenated resource and literal",
+                "abc!string/multiple_labels",
+                "abcabc", "def", "ghi");
+    }
+
+    public void testSplitIndirectReference() {
+        assertTextArray("Indirect",
+                "!string/indirect_string", "a", "b", "c");
+        assertTextArray("Indirect with literal",
+                "1,!string/indirect_string_with_literal,2", "1", "x", "a", "b", "c", "y", "2");
+        assertTextArray("Indirect2",
+                "!string/indirect2_string", "a", "b", "c");
+    }
+
+    public void testSplitInfiniteIndirectReference() {
+        assertError("Infinite indirection",
+                "1,!string/infinite_indirection,2", "1", "infinite", "<infinite>", "loop", "2");
+    }
+
+    public void testLabelReferece() {
+        assertTextArray("Indirect naviagte actions as more key",
+                "!string/keyspec_indirect_navigate_actions",
+                "!fixedColumnOrder!2",
+                "!hasLabels!", "ActionPrevious|!code/key_action_previous",
+                "!hasLabels!", "ActionNext|!code/key_action_next");
+    }
+
+    public void testUselessUpperCaseSpecifier() {
+        assertTextArray("EMPTY STRING",
+                "!STRING/EMPTY_STRING", "!STRING/EMPTY_STRING");
+
+        assertTextArray("SINGLE CHAR",
+                "!STRING/SINGLE_CHAR", "!STRING/SINGLE_CHAR");
+        assertTextArray("Escape and SINGLE CHAR",
+                "\\\\!STRING/SINGLE_CHAR", "\\\\!STRING/SINGLE_CHAR");
+
+        assertTextArray("MULTIPLE CHARS",
+                "!STRING/MULTIPLE_CHARS", "!STRING/MULTIPLE_CHARS");
+
+        assertTextArray("Literals and RESOURCES",
+                "1,!STRING/MULTIPLE_CHARS,z", "1", "!STRING/MULTIPLE_CHARS", "z");
+        assertTextArray("Multiple single RESOURCE chars and LABELS 2",
+                "!STRING/SINGLE_CHAR,!STRING/SINGLE_LABEL,!STRING/ESCAPED_COMMA_ESCAPE",
+                "!STRING/SINGLE_CHAR", "!STRING/SINGLE_LABEL", "!STRING/ESCAPED_COMMA_ESCAPE");
+
+        assertTextArray("INDIRECT",
+                "!STRING/INDIRECT_STRING", "!STRING/INDIRECT_STRING");
+        assertTextArray("INDIRECT with literal",
+                "1,!STRING/INDIRECT_STRING_WITH_LITERAL,2",
+                "1", "!STRING/INDIRECT_STRING_WITH_LITERAL", "2");
+        assertTextArray("INDIRECT2",
+                "!STRING/INDIRECT2_STRING", "!STRING/INDIRECT2_STRING");
+
+        assertTextArray("Upper indirect",
+                "!string/upper_indirect_string", "!STRING/MULTIPLE_CHARS");
+        assertTextArray("Upper indirect with literal",
+                "1,!string/upper_indirect_string_with_literal,2",
+                "1", "x", "!STRING/MULTIPLE_CHARS", "y", "2");
+        assertTextArray("Upper indirect2",
+                "!string/upper_indirect2_string", "!STRING/UPPER_INDIRECT_STRING");
+
+        assertTextArray("UPPER INDIRECT",
+                "!STRING/upper_INDIRECT_STRING", "!STRING/upper_INDIRECT_STRING");
+        assertTextArray("Upper INDIRECT with literal",
+                "1,!STRING/upper_INDIRECT_STRING_WITH_LITERAL,2",
+                "1", "!STRING/upper_INDIRECT_STRING_WITH_LITERAL", "2");
+        assertTextArray("Upper INDIRECT2",
+                "!STRING/upper_INDIRECT2_STRING", "!STRING/upper_INDIRECT2_STRING");
+
+        assertTextArray("INFINITE INDIRECTION",
+                "1,!STRING/INFINITE_INDIRECTION,2", "1", "!STRING/INFINITE_INDIRECTION", "2");
+
+        assertTextArray("Upper infinite indirection",
+                "1,!string/upper_infinite_indirection,2",
+                "1", "infinite", "!STRING/INFINITE_INDIRECTION", "loop", "2");
+        assertTextArray("Upper INFINITE INDIRECTION",
+                "1,!STRING/UPPER_INFINITE_INDIRECTION,2",
+                "1", "!STRING/UPPER_INFINITE_INDIRECTION", "2");
+
+        assertTextArray("INDIRECT NAVIGATE ACTIONS AS MORE KEY",
+                "!STRING/INDIRECT_NAVIGATE_ACTIONS_AS_MORE_KEY",
+                "!STRING/INDIRECT_NAVIGATE_ACTIONS_AS_MORE_KEY");
+     }
+}
diff --git a/tools/make-keyboard-text/res/values-hi-rZZ/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values-hi-rZZ/donottranslate-more-keys.xml
index 068639d77d..50834e0fc9 100644
--- a/tools/make-keyboard-text/res/values-hi-rZZ/donottranslate-more-keys.xml
+++ b/tools/make-keyboard-text/res/values-hi-rZZ/donottranslate-more-keys.xml
@@ -20,4 +20,12 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <!-- U+20B9: "₹" INDIAN RUPEE SIGN -->
     <string name="keyspec_currency">&#x20B9;</string>
+    <string name="label_go_key">Go</string>
+    <string name="label_send_key">Send</string>
+    <string name="label_next_key">Next</string>
+    <string name="label_done_key">Done</string>
+    <string name="label_search_key">Search</string>
+    <string name="label_previous_key">Prev</string>
+    <string name="label_pause_key">Pause</string>
+    <string name="label_wait_key">Wait</string>
 </resources>
diff --git a/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml
index c4a1b889e4..b6da7d13d6 100644
--- a/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml
+++ b/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml
@@ -259,4 +259,12 @@
     <string name="morekeys_double_quote">!fixedColumnOrder!5,!text/double_quotes,!text/double_angle_quotes</string>
     <string name="morekeys_tablet_double_quote">!fixedColumnOrder!6,!text/double_quotes,!text/single_quotes,!text/double_angle_quotes,!text/single_angle_quotes</string>
     <string name="keyspec_emoji_action_key">!icon/emoji_action_key|!code/key_emoji</string>
+    <string name="label_go_key">!string/label_go_key</string>
+    <string name="label_send_key">!string/label_send_key</string>
+    <string name="label_next_key">!string/label_next_key</string>
+    <string name="label_done_key">!string/label_done_key</string>
+    <string name="label_search_key">!string/label_search_key</string>
+    <string name="label_previous_key">!string/label_previous_key</string>
+    <string name="label_pause_key">!string/label_pause_key</string>
+    <string name="label_wait_key">!string/label_wait_key</string>
 </resources>
-- 
GitLab