Merge "Add important notice strip"
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 5fd70ea..f7eec6b 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -284,19 +284,19 @@
         int moreKeysColumn = style.getInt(keyAttr,
                 R.styleable.Keyboard_Key_maxMoreKeysColumn, params.mMaxMoreKeysKeyboardColumn);
         int value;
-        if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
+        if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
             moreKeysColumn = value & MORE_KEYS_COLUMN_MASK;
         }
-        if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
+        if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
             moreKeysColumn = MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER | (value & MORE_KEYS_COLUMN_MASK);
         }
-        if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
+        if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
             moreKeysColumn |= MORE_KEYS_FLAGS_HAS_LABELS;
         }
-        if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) {
+        if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) {
             moreKeysColumn |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS;
         }
-        if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
+        if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
             moreKeysColumn |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY;
         }
         mMoreKeysColumnAndFlags = moreKeysColumn;
@@ -308,7 +308,7 @@
             additionalMoreKeys = style.getStringArray(keyAttr,
                     R.styleable.Keyboard_Key_additionalMoreKeys);
         }
-        moreKeys = KeySpecParser.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
+        moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
         if (moreKeys != null) {
             actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
             mMoreKeys = new MoreKeySpec[moreKeys.length];
diff --git a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
index 4ccecb2..dce7fc5 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.keyboard.internal;
 
 import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.utils.StringUtils;
 
 import android.text.TextUtils;
 
@@ -29,15 +30,16 @@
  * marker. An output text may consist of multiple code points separated by comma.
  * The format of the codesArray element should be:
  * <pre>
- *   codePointInHex[,codePoint2InHex]*(|outputTextCodePointInHex[,outputTextCodePoint2InHex]*)?
+ *   label1[,label2]*(|outputText1[,outputText2]*(|minSupportSdkVersion)?)?
  * </pre>
  */
 // TODO: Write unit tests for this class.
 public final class CodesArrayParser {
     // Constants for parsing.
-    private static final char COMMA = ',';
-    private static final String VERTICAL_BAR_STRING = "\\|";
-    private static final String COMMA_STRING = ",";
+    private static final char COMMA = Constants.CODE_COMMA;
+    private static final String COMMA_REGEX = StringUtils.newSingleCodePointString(COMMA);
+    private static final String VERTICAL_BAR_REGEX = // "\\|"
+            new String(new char[] { Constants.CODE_BACKSLASH, Constants.CODE_VERTICAL_BAR });
     private static final int BASE_HEX = 16;
 
     private CodesArrayParser() {
@@ -45,7 +47,7 @@
     }
 
     private static String getLabelSpec(final String codesArraySpec) {
-        final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1);
+        final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1);
         if (strs.length <= 1) {
             return codesArraySpec;
         }
@@ -55,7 +57,7 @@
     public static String parseLabel(final String codesArraySpec) {
         final String labelSpec = getLabelSpec(codesArraySpec);
         final StringBuilder sb = new StringBuilder();
-        for (final String codeInHex : labelSpec.split(COMMA_STRING)) {
+        for (final String codeInHex : labelSpec.split(COMMA_REGEX)) {
             final int codePoint = Integer.parseInt(codeInHex, BASE_HEX);
             sb.appendCodePoint(codePoint);
         }
@@ -63,17 +65,15 @@
     }
 
     private static String getCodeSpec(final String codesArraySpec) {
-        final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1);
+        final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1);
         if (strs.length <= 1) {
             return codesArraySpec;
         }
         return TextUtils.isEmpty(strs[1]) ? strs[0] : strs[1];
     }
 
-    // codesArraySpec consists of:
-    // <label>|<code0>,<code1>,...|<minSupportSdkVersion>
     public static int getMinSupportSdkVersion(final String codesArraySpec) {
-        final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1);
+        final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1);
         if (strs.length <= 2) {
             return 0;
         }
@@ -98,7 +98,7 @@
             return null;
         }
         final StringBuilder sb = new StringBuilder();
-        for (final String codeInHex : codeSpec.split(COMMA_STRING)) {
+        for (final String codeInHex : codeSpec.split(COMMA_REGEX)) {
             final int codePoint = Integer.parseInt(codeInHex, BASE_HEX);
             sb.appendCodePoint(codePoint);
         }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
index 5c288c8..c51941d 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
@@ -19,112 +19,47 @@
 import static com.android.inputmethod.latin.Constants.CODE_OUTPUT_TEXT;
 import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED;
 
-import android.text.TextUtils;
-
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.utils.StringUtils;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-
 /**
- * The string parser of more keys specification.
- * The specification is comma separated texts each of which represents one "more key".
- * The specification might have label or string resource reference in it. These references are
- * expanded before parsing comma.
- * - Label reference should be a string representation of label (!text/label_name)
- * - String resource reference should be a string representation of resource (!text/resource_name)
- * Each "more key" specification is one of the following:
- * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText).
- * - Icon followed by keyOutputText or code (!icon/icon_name|!code/code_name)
- *   - Icon should be a string representation of icon (!icon/icon_name).
- *   - Code should be a code point presented by hexadecimal string prefixed with "0x", or a string
- *     representation of code (!code/code_name).
+ * The string parser of the key specification.
+ *
+ * Each key specification is one of the following:
+ * - Label optionally followed by keyOutputText (keyLabel|keyOutputText).
+ * - Label optionally followed by code point (keyLabel|!code/code_name).
+ * - Icon followed by keyOutputText (!icon/icon_name|keyOutputText).
+ * - Icon followed by code point (!icon/icon_name|!code/code_name).
+ * Label and keyOutputText are one of the following:
+ * - Literal string.
+ * - Label reference represented by (!text/label_name), see {@link KeyboardTextsSet}.
+ * - String resource reference represented by (!text/resource_name), see {@link KeyboardTextsSet}.
+ * Icon is represented by (!icon/icon_name), see {@link KeyboardIconsSet}.
+ * Code is one of the following:
+ * - Code point presented by hexadecimal string prefixed with "0x"
+ * - Code reference represented by (!code/code_name), see {@link KeyboardCodesSet}.
  * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character.
- * Note that the '\' is also parsed by XML parser and CSV parser as well.
- * See {@link KeyboardIconsSet} about icon_name.
+ * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
+ * as well.
  */
 public final class KeySpecParser {
-    private static final boolean DEBUG = LatinImeLogger.sDBG;
-
-    private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
-
     // Constants for parsing.
-    private static final char COMMA = ',';
-    private static final char BACKSLASH = '\\';
-    private static final char VERTICAL_BAR = '|';
-    private static final String PREFIX_TEXT = "!text/";
-    static final String PREFIX_ICON = "!icon/";
-    private static final String PREFIX_CODE = "!code/";
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
+    private static final char VERTICAL_BAR = Constants.CODE_VERTICAL_BAR;
     private static final String PREFIX_HEX = "0x";
-    private static final String ADDITIONAL_MORE_KEY_MARKER = "%";
 
     private KeySpecParser() {
         // Intentional empty constructor for utility class.
     }
 
-    /**
-     * Split the text containing multiple key specifications separated by commas into an array of
-     * key specifications.
-     * A key specification can contain a character escaped by the backslash character, including a
-     * comma character.
-     * Note that an empty key specification will be eliminated from the result array.
-     *
-     * @param text the text containing multiple key specifications.
-     * @return an array of key specification text. Null if the specified <code>text</code> is empty
-     * or has no key specifications.
-     */
-    public static String[] splitKeySpecs(final String text) {
-        if (TextUtils.isEmpty(text)) {
-            return null;
-        }
-        final int size = text.length();
-        // Optimization for one-letter key specification.
-        if (size == 1) {
-            return text.charAt(0) == COMMA ? null : new String[] { text };
-        }
-
-        ArrayList<String> list = null;
-        int start = 0;
-        // The characters in question in this loop are COMMA and BACKSLASH. These characters never
-        // match any high or low surrogate character. So it is OK to iterate through with char
-        // index.
-        for (int pos = 0; pos < size; pos++) {
-            final char c = text.charAt(pos);
-            if (c == COMMA) {
-                // Skip empty entry.
-                if (pos - start > 0) {
-                    if (list == null) {
-                        list = CollectionUtils.newArrayList();
-                    }
-                    list.add(text.substring(start, pos));
-                }
-                // Skip comma
-                start = pos + 1;
-            } else if (c == BACKSLASH) {
-                // Skip escape character and escaped character.
-                pos++;
-            }
-        }
-        final String remain = (size - start > 0) ? text.substring(start) : null;
-        if (list == null) {
-            return remain != null ? new String[] { remain } : null;
-        }
-        if (remain != null) {
-            list.add(remain);
-        }
-        return list.toArray(new String[list.size()]);
+    private static boolean hasIcon(final String keySpec) {
+        return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON);
     }
 
-    private static boolean hasIcon(final String moreKeySpec) {
-        return moreKeySpec.startsWith(PREFIX_ICON);
-    }
-
-    private static boolean hasCode(final String moreKeySpec) {
-        final int end = indexOfLabelEnd(moreKeySpec, 0);
-        if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith(
-                PREFIX_CODE, end + 1)) {
+    private static boolean hasCode(final String keySpec) {
+        final int end = indexOfLabelEnd(keySpec, 0);
+        if (end > 0 && end + 1 < keySpec.length() && keySpec.startsWith(
+                KeyboardCodesSet.PREFIX_CODE, end + 1)) {
             return true;
         }
         return false;
@@ -149,17 +84,17 @@
         return sb.toString();
     }
 
-    private static int indexOfLabelEnd(final String moreKeySpec, final int start) {
-        if (moreKeySpec.indexOf(BACKSLASH, start) < 0) {
-            final int end = moreKeySpec.indexOf(VERTICAL_BAR, start);
+    private static int indexOfLabelEnd(final String keySpec, final int start) {
+        if (keySpec.indexOf(BACKSLASH, start) < 0) {
+            final int end = keySpec.indexOf(VERTICAL_BAR, start);
             if (end == 0) {
-                throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + moreKeySpec);
+                throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + keySpec);
             }
             return end;
         }
-        final int length = moreKeySpec.length();
+        final int length = keySpec.length();
         for (int pos = start; pos < length; pos++) {
-            final char c = moreKeySpec.charAt(pos);
+            final char c = keySpec.charAt(pos);
             if (c == BACKSLASH && pos + 1 < length) {
                 // Skip escape char
                 pos++;
@@ -170,63 +105,63 @@
         return -1;
     }
 
-    public static String getLabel(final String moreKeySpec) {
-        if (hasIcon(moreKeySpec)) {
+    public static String getLabel(final String keySpec) {
+        if (hasIcon(keySpec)) {
             return null;
         }
-        final int end = indexOfLabelEnd(moreKeySpec, 0);
-        final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end))
-                : parseEscape(moreKeySpec);
-        if (TextUtils.isEmpty(label)) {
-            throw new KeySpecParserError("Empty label: " + moreKeySpec);
+        final int end = indexOfLabelEnd(keySpec, 0);
+        final String label = (end > 0) ? parseEscape(keySpec.substring(0, end))
+                : parseEscape(keySpec);
+        if (label.isEmpty()) {
+            throw new KeySpecParserError("Empty label: " + keySpec);
         }
         return label;
     }
 
-    private static String getOutputTextInternal(final String moreKeySpec) {
-        final int end = indexOfLabelEnd(moreKeySpec, 0);
+    private static String getOutputTextInternal(final String keySpec) {
+        final int end = indexOfLabelEnd(keySpec, 0);
         if (end <= 0) {
             return null;
         }
-        if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
-            throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec);
+        if (indexOfLabelEnd(keySpec, end + 1) >= 0) {
+            throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec);
         }
-        return parseEscape(moreKeySpec.substring(end + /* VERTICAL_BAR */1));
+        return parseEscape(keySpec.substring(end + /* VERTICAL_BAR */1));
     }
 
-    static String getOutputText(final String moreKeySpec) {
-        if (hasCode(moreKeySpec)) {
+    public static String getOutputText(final String keySpec) {
+        if (hasCode(keySpec)) {
             return null;
         }
-        final String outputText = getOutputTextInternal(moreKeySpec);
+        final String outputText = getOutputTextInternal(keySpec);
         if (outputText != null) {
             if (StringUtils.codePointCount(outputText) == 1) {
                 // If output text is one code point, it should be treated as a code.
                 // See {@link #getCode(Resources, String)}.
                 return null;
             }
-            if (!TextUtils.isEmpty(outputText)) {
-                return outputText;
+            if (outputText.isEmpty()) {
+                throw new KeySpecParserError("Empty outputText: " + keySpec);
             }
-            throw new KeySpecParserError("Empty outputText: " + moreKeySpec);
+            return outputText;
         }
-        final String label = getLabel(moreKeySpec);
+        final String label = getLabel(keySpec);
         if (label == null) {
-            throw new KeySpecParserError("Empty label: " + moreKeySpec);
+            throw new KeySpecParserError("Empty label: " + keySpec);
         }
         // Code is automatically generated for one letter label. See {@link getCode()}.
         return (StringUtils.codePointCount(label) == 1) ? null : label;
     }
 
-    static int getCode(final String moreKeySpec, final KeyboardCodesSet codesSet) {
-        if (hasCode(moreKeySpec)) {
-            final int end = indexOfLabelEnd(moreKeySpec, 0);
-            if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
-                throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec);
+    public static int getCode(final String keySpec, final KeyboardCodesSet codesSet) {
+        if (hasCode(keySpec)) {
+            final int end = indexOfLabelEnd(keySpec, 0);
+            if (indexOfLabelEnd(keySpec, end + 1) >= 0) {
+                throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec);
             }
-            return parseCode(moreKeySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED);
+            return parseCode(keySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED);
         }
-        final String outputText = getOutputTextInternal(moreKeySpec);
+        final String outputText = getOutputTextInternal(keySpec);
         if (outputText != null) {
             // If output text is one code point, it should be treated as a code.
             // See {@link #getOutputText(String)}.
@@ -235,7 +170,7 @@
             }
             return CODE_OUTPUT_TEXT;
         }
-        final String label = getLabel(moreKeySpec);
+        final String label = getLabel(keySpec);
         // Code is automatically generated for one letter label.
         if (StringUtils.codePointCount(label) == 1) {
             return label.codePointAt(0);
@@ -246,8 +181,8 @@
     public static int parseCode(final String text, final KeyboardCodesSet codesSet,
             final int defCode) {
         if (text == null) return defCode;
-        if (text.startsWith(PREFIX_CODE)) {
-            return codesSet.getCode(text.substring(PREFIX_CODE.length()));
+        if (text.startsWith(KeyboardCodesSet.PREFIX_CODE)) {
+            return codesSet.getCode(text.substring(KeyboardCodesSet.PREFIX_CODE.length()));
         } else if (text.startsWith(PREFIX_HEX)) {
             return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16);
         } else {
@@ -255,214 +190,22 @@
         }
     }
 
-    public static int getIconId(final String moreKeySpec) {
-        if (moreKeySpec != null && hasIcon(moreKeySpec)) {
-            final int end = moreKeySpec.indexOf(VERTICAL_BAR, PREFIX_ICON.length());
-            final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length())
-                    : moreKeySpec.substring(PREFIX_ICON.length(), end);
+    public static int getIconId(final String keySpec) {
+        if (keySpec != null && hasIcon(keySpec)) {
+            final int end = keySpec.indexOf(
+                    VERTICAL_BAR, KeyboardIconsSet.PREFIX_ICON.length());
+            final String name = (end < 0)
+                    ? keySpec.substring(KeyboardIconsSet.PREFIX_ICON.length())
+                    : keySpec.substring(KeyboardIconsSet.PREFIX_ICON.length(), end);
             return KeyboardIconsSet.getIconId(name);
         }
         return KeyboardIconsSet.ICON_UNDEFINED;
     }
 
-    private static final String[] EMPTY_STRING_ARRAY = new String[0];
-
-    private static String[] filterOutEmptyString(final String[] array) {
-        if (array == null) {
-            return EMPTY_STRING_ARRAY;
-        }
-        ArrayList<String> out = null;
-        for (int i = 0; i < array.length; i++) {
-            final String entry = array[i];
-            if (TextUtils.isEmpty(entry)) {
-                if (out == null) {
-                    out = CollectionUtils.arrayAsList(array, 0, i);
-                }
-            } else if (out != null) {
-                out.add(entry);
-            }
-        }
-        if (out == null) {
-            return array;
-        }
-        return out.toArray(new String[out.size()]);
-    }
-
-    public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
-            final String[] additionalMoreKeySpecs) {
-        final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
-        final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
-        final int moreKeysCount = moreKeys.length;
-        final int additionalCount = additionalMoreKeys.length;
-        ArrayList<String> out = null;
-        int additionalIndex = 0;
-        for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
-            final String moreKeySpec = moreKeys[moreKeyIndex];
-            if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
-                if (additionalIndex < additionalCount) {
-                    // Replace '%' marker with additional more key specification.
-                    final String additionalMoreKey = additionalMoreKeys[additionalIndex];
-                    if (out != null) {
-                        out.add(additionalMoreKey);
-                    } else {
-                        moreKeys[moreKeyIndex] = additionalMoreKey;
-                    }
-                    additionalIndex++;
-                } else {
-                    // Filter out excessive '%' marker.
-                    if (out == null) {
-                        out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
-                    }
-                }
-            } else {
-                if (out != null) {
-                    out.add(moreKeySpec);
-                }
-            }
-        }
-        if (additionalCount > 0 && additionalIndex == 0) {
-            // No '%' marker is found in more keys.
-            // Insert all additional more keys to the head of more keys.
-            if (DEBUG && out != null) {
-                throw new RuntimeException("Internal logic error:"
-                        + " moreKeys=" + Arrays.toString(moreKeys)
-                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
-            }
-            out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
-            for (int i = 0; i < moreKeysCount; i++) {
-                out.add(moreKeys[i]);
-            }
-        } else if (additionalIndex < additionalCount) {
-            // The number of '%' markers are less than additional more keys.
-            // Append remained additional more keys to the tail of more keys.
-            if (DEBUG && out != null) {
-                throw new RuntimeException("Internal logic error:"
-                        + " moreKeys=" + Arrays.toString(moreKeys)
-                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
-            }
-            out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
-            for (int i = additionalIndex; i < additionalCount; i++) {
-                out.add(additionalMoreKeys[additionalIndex]);
-            }
-        }
-        if (out == null && moreKeysCount > 0) {
-            return moreKeys;
-        } else if (out != null && out.size() > 0) {
-            return out.toArray(new String[out.size()]);
-        } else {
-            return null;
-        }
-    }
-
     @SuppressWarnings("serial")
     public static final class KeySpecParserError extends RuntimeException {
         public KeySpecParserError(final String message) {
             super(message);
         }
     }
-
-    public static String resolveTextReference(final String rawText,
-            final KeyboardTextsSet textsSet) {
-        if (TextUtils.isEmpty(rawText)) {
-            return null;
-        }
-        int level = 0;
-        String text = rawText;
-        StringBuilder sb;
-        do {
-            level++;
-            if (level >= MAX_STRING_REFERENCE_INDIRECTION) {
-                throw new RuntimeException("too many @string/resource indirection: " + text);
-            }
-
-            final int prefixLen = PREFIX_TEXT.length();
-            final int size = text.length();
-            if (size < prefixLen) {
-                return TextUtils.isEmpty(text) ? null : text;
-            }
-
-            sb = null;
-            for (int pos = 0; pos < size; pos++) {
-                final char c = text.charAt(pos);
-                if (text.startsWith(PREFIX_TEXT, pos) && textsSet != null) {
-                    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(textsSet.getText(name));
-                    pos = end - 1;
-                } else if (c == BACKSLASH) {
-                    if (sb != null) {
-                        // Append both escape character and escaped character.
-                        sb.append(text.substring(pos, Math.min(pos + 2, size)));
-                    }
-                    pos++;
-                } else if (sb != null) {
-                    sb.append(c);
-                }
-            }
-
-            if (sb != null) {
-                text = sb.toString();
-            }
-        } while (sb != null);
-        return TextUtils.isEmpty(text) ? null : text;
-    }
-
-    private static int searchTextNameEnd(final String text, final int start) {
-        final int size = text.length();
-        for (int pos = start; pos < size; pos++) {
-            final char c = text.charAt(pos);
-            // Label name should be consisted of [a-zA-Z_0-9].
-            if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) {
-                continue;
-            }
-            return pos;
-        }
-        return size;
-    }
-
-    public static int getIntValue(final String[] moreKeys, final String key,
-            final int defaultValue) {
-        if (moreKeys == null) {
-            return defaultValue;
-        }
-        final int keyLen = key.length();
-        boolean foundValue = false;
-        int value = defaultValue;
-        for (int i = 0; i < moreKeys.length; i++) {
-            final String moreKeySpec = moreKeys[i];
-            if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
-                continue;
-            }
-            moreKeys[i] = null;
-            try {
-                if (!foundValue) {
-                    value = Integer.parseInt(moreKeySpec.substring(keyLen));
-                    foundValue = true;
-                }
-            } catch (NumberFormatException e) {
-                throw new RuntimeException(
-                        "integer should follow after " + key + ": " + moreKeySpec);
-            }
-        }
-        return value;
-    }
-
-    public static boolean getBooleanValue(final String[] moreKeys, final String key) {
-        if (moreKeys == null) {
-            return false;
-        }
-        boolean value = false;
-        for (int i = 0; i < moreKeys.length; i++) {
-            final String moreKeySpec = moreKeys[i];
-            if (moreKeySpec == null || !moreKeySpec.equals(key)) {
-                continue;
-            }
-            moreKeys[i] = null;
-            value = true;
-        }
-        return value;
-    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
index e6a6743..7941ddd 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
@@ -32,15 +32,15 @@
 
     protected String parseString(final TypedArray a, final int index) {
         if (a.hasValue(index)) {
-            return KeySpecParser.resolveTextReference(a.getString(index), mTextsSet);
+            return mTextsSet.resolveTextReference(a.getString(index));
         }
         return null;
     }
 
     protected String[] parseStringArray(final TypedArray a, final int index) {
         if (a.hasValue(index)) {
-            final String text = KeySpecParser.resolveTextReference(a.getString(index), mTextsSet);
-            return KeySpecParser.splitKeySpecs(text);
+            final String text = mTextsSet.resolveTextReference(a.getString(index));
+            return MoreKeySpec.splitKeySpecs(text);
         }
         return null;
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
index 78809d5..aeb4db5 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
@@ -22,6 +22,8 @@
 import java.util.HashMap;
 
 public final class KeyboardCodesSet {
+    public static final String PREFIX_CODE = "!code/";
+
     private static final HashMap<String, int[]> sLanguageToCodesMap = CollectionUtils.newHashMap();
     private static final HashMap<String, Integer> sNameToIdMap = CollectionUtils.newHashMap();
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
index 0ee935f..da8bf7d 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
@@ -30,6 +30,7 @@
 public final class KeyboardIconsSet {
     private static final String TAG = KeyboardIconsSet.class.getSimpleName();
 
+    public static final String PREFIX_ICON = "!icon/";
     public static final int ICON_UNDEFINED = 0;
     private static final int ATTR_UNDEFINED = 0;
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
index 4e8eb3e..f8c0988 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -18,8 +18,10 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.text.TextUtils;
 
 import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.util.HashMap;
@@ -45,6 +47,10 @@
  *   KeyboardTextsSet.java
  */
 public final class KeyboardTextsSet {
+    public static final String PREFIX_TEXT = "!text/";
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
+    private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
+
     // Language to texts map.
     private static final HashMap<String, String[]> sLocaleToTextsMap = CollectionUtils.newHashMap();
     private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap();
@@ -87,6 +93,67 @@
         return (text == null) ? LANGUAGE_DEFAULT[id] : text;
     }
 
+    private static int searchTextNameEnd(final String text, final int start) {
+        final int size = text.length();
+        for (int pos = start; pos < size; pos++) {
+            final char c = text.charAt(pos);
+            // Label name should be consisted of [a-zA-Z_0-9].
+            if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) {
+                continue;
+            }
+            return pos;
+        }
+        return size;
+    }
+
+    public String resolveTextReference(final String rawText) {
+        if (TextUtils.isEmpty(rawText)) {
+            return null;
+        }
+        int level = 0;
+        String text = rawText;
+        StringBuilder sb;
+        do {
+            level++;
+            if (level >= MAX_STRING_REFERENCE_INDIRECTION) {
+                throw new RuntimeException("too many @string/resource indirection: " + text);
+            }
+
+            final int prefixLen = PREFIX_TEXT.length();
+            final int size = text.length();
+            if (size < prefixLen) {
+                return TextUtils.isEmpty(text) ? null : text;
+            }
+
+            sb = null;
+            for (int pos = 0; pos < size; pos++) {
+                final char c = text.charAt(pos);
+                if (text.startsWith(PREFIX_TEXT, pos)) {
+                    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;
+                } else if (c == BACKSLASH) {
+                    if (sb != null) {
+                        // Append both escape character and escaped character.
+                        sb.append(text.substring(pos, Math.min(pos + 2, size)));
+                    }
+                    pos++;
+                } else if (sb != null) {
+                    sb.append(c);
+                }
+            }
+
+            if (sb != null) {
+                text = sb.toString();
+            }
+        } while (sb != null);
+        return TextUtils.isEmpty(text) ? null : text;
+    }
+
     // These texts' name should be aligned with the @string/<name> in
     // values*/strings-action-keys.xml.
     private static final String[] RESOURCE_NAMES = {
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
index 889fc9c..d3bc0c2 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
@@ -19,10 +19,25 @@
 import android.text.TextUtils;
 
 import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.StringUtils;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Locale;
 
+/**
+ * The more key specification object. The more keys are an array of {@link MoreKeySpec}.
+ *
+ * The more keys specification is comma separated "key specification" each of which represents one
+ * "more key".
+ * The key specification might have label or string resource reference in it. These references are
+ * expanded before parsing comma.
+ * Special character, comma ',' backslash '\' can be escaped by '\' character.
+ * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
+ * as well.
+ */
 public final class MoreKeySpec {
     public final int mCode;
     public final String mLabel;
@@ -74,7 +89,7 @@
     @Override
     public String toString() {
         final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel
-                : KeySpecParser.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId));
+                : KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId));
         final String output = (mCode == Constants.CODE_OUTPUT_TEXT ? mOutputText
                 : Constants.printableCode(mCode));
         if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) {
@@ -83,4 +98,196 @@
             return label + "|" + output;
         }
     }
+
+    private static final boolean DEBUG = LatinImeLogger.sDBG;
+    // Constants for parsing.
+    private static final char COMMA = Constants.CODE_COMMA;
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
+    private static final String ADDITIONAL_MORE_KEY_MARKER =
+            StringUtils.newSingleCodePointString(Constants.CODE_PERCENT);
+
+    /**
+     * Split the text containing multiple key specifications separated by commas into an array of
+     * key specifications.
+     * A key specification can contain a character escaped by the backslash character, including a
+     * comma character.
+     * Note that an empty key specification will be eliminated from the result array.
+     *
+     * @param text the text containing multiple key specifications.
+     * @return an array of key specification text. Null if the specified <code>text</code> is empty
+     * or has no key specifications.
+     */
+    public static String[] splitKeySpecs(final String text) {
+        if (TextUtils.isEmpty(text)) {
+            return null;
+        }
+        final int size = text.length();
+        // Optimization for one-letter key specification.
+        if (size == 1) {
+            return text.charAt(0) == COMMA ? null : new String[] { text };
+        }
+
+        ArrayList<String> list = null;
+        int start = 0;
+        // The characters in question in this loop are COMMA and BACKSLASH. These characters never
+        // match any high or low surrogate character. So it is OK to iterate through with char
+        // index.
+        for (int pos = 0; pos < size; pos++) {
+            final char c = text.charAt(pos);
+            if (c == COMMA) {
+                // Skip empty entry.
+                if (pos - start > 0) {
+                    if (list == null) {
+                        list = CollectionUtils.newArrayList();
+                    }
+                    list.add(text.substring(start, pos));
+                }
+                // Skip comma
+                start = pos + 1;
+            } else if (c == BACKSLASH) {
+                // Skip escape character and escaped character.
+                pos++;
+            }
+        }
+        final String remain = (size - start > 0) ? text.substring(start) : null;
+        if (list == null) {
+            return remain != null ? new String[] { remain } : null;
+        }
+        if (remain != null) {
+            list.add(remain);
+        }
+        return list.toArray(new String[list.size()]);
+    }
+
+    private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+    private static String[] filterOutEmptyString(final String[] array) {
+        if (array == null) {
+            return EMPTY_STRING_ARRAY;
+        }
+        ArrayList<String> out = null;
+        for (int i = 0; i < array.length; i++) {
+            final String entry = array[i];
+            if (TextUtils.isEmpty(entry)) {
+                if (out == null) {
+                    out = CollectionUtils.arrayAsList(array, 0, i);
+                }
+            } else if (out != null) {
+                out.add(entry);
+            }
+        }
+        if (out == null) {
+            return array;
+        }
+        return out.toArray(new String[out.size()]);
+    }
+
+    public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
+            final String[] additionalMoreKeySpecs) {
+        final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
+        final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
+        final int moreKeysCount = moreKeys.length;
+        final int additionalCount = additionalMoreKeys.length;
+        ArrayList<String> out = null;
+        int additionalIndex = 0;
+        for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
+            final String moreKeySpec = moreKeys[moreKeyIndex];
+            if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
+                if (additionalIndex < additionalCount) {
+                    // Replace '%' marker with additional more key specification.
+                    final String additionalMoreKey = additionalMoreKeys[additionalIndex];
+                    if (out != null) {
+                        out.add(additionalMoreKey);
+                    } else {
+                        moreKeys[moreKeyIndex] = additionalMoreKey;
+                    }
+                    additionalIndex++;
+                } else {
+                    // Filter out excessive '%' marker.
+                    if (out == null) {
+                        out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
+                    }
+                }
+            } else {
+                if (out != null) {
+                    out.add(moreKeySpec);
+                }
+            }
+        }
+        if (additionalCount > 0 && additionalIndex == 0) {
+            // No '%' marker is found in more keys.
+            // Insert all additional more keys to the head of more keys.
+            if (DEBUG && out != null) {
+                throw new RuntimeException("Internal logic error:"
+                        + " moreKeys=" + Arrays.toString(moreKeys)
+                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
+            }
+            out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
+            for (int i = 0; i < moreKeysCount; i++) {
+                out.add(moreKeys[i]);
+            }
+        } else if (additionalIndex < additionalCount) {
+            // The number of '%' markers are less than additional more keys.
+            // Append remained additional more keys to the tail of more keys.
+            if (DEBUG && out != null) {
+                throw new RuntimeException("Internal logic error:"
+                        + " moreKeys=" + Arrays.toString(moreKeys)
+                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
+            }
+            out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
+            for (int i = additionalIndex; i < additionalCount; i++) {
+                out.add(additionalMoreKeys[additionalIndex]);
+            }
+        }
+        if (out == null && moreKeysCount > 0) {
+            return moreKeys;
+        } else if (out != null && out.size() > 0) {
+            return out.toArray(new String[out.size()]);
+        } else {
+            return null;
+        }
+    }
+
+    public static int getIntValue(final String[] moreKeys, final String key,
+            final int defaultValue) {
+        if (moreKeys == null) {
+            return defaultValue;
+        }
+        final int keyLen = key.length();
+        boolean foundValue = false;
+        int value = defaultValue;
+        for (int i = 0; i < moreKeys.length; i++) {
+            final String moreKeySpec = moreKeys[i];
+            if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
+                continue;
+            }
+            moreKeys[i] = null;
+            try {
+                if (!foundValue) {
+                    value = Integer.parseInt(moreKeySpec.substring(keyLen));
+                    foundValue = true;
+                }
+            } catch (NumberFormatException e) {
+                throw new RuntimeException(
+                        "integer should follow after " + key + ": " + moreKeySpec);
+            }
+        }
+        return value;
+    }
+
+    public static boolean getBooleanValue(final String[] moreKeys, final String key) {
+        if (moreKeys == null) {
+            return false;
+        }
+        boolean value = false;
+        for (int i = 0; i < moreKeys.length; i++) {
+            final String moreKeySpec = moreKeys[i];
+            if (moreKeySpec == null || !moreKeySpec.equals(key)) {
+                continue;
+            }
+            moreKeys[i] = null;
+            value = true;
+        }
+        return value;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index 0b396b1..d6ac71f 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -191,6 +191,8 @@
     public static final int CODE_QUESTION_MARK = '?';
     public static final int CODE_EXCLAMATION_MARK = '!';
     public static final int CODE_SLASH = '/';
+    public static final int CODE_BACKSLASH = '\\';
+    public static final int CODE_VERTICAL_BAR = '|';
     public static final int CODE_COMMERCIAL_AT = '@';
     public static final int CODE_PLUS = '+';
     public static final int CODE_PERCENT = '%';
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 8e1675b..4dee84a 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -267,9 +267,9 @@
 
     protected Map<String, String> getHeaderAttributeMap() {
         HashMap<String, String> attributeMap = new HashMap<String, String>();
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mDictName);
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale.toString());
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE,
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_KEY, mDictName);
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY, mLocale.toString());
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_KEY,
                 String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
         return attributeMap;
     }
diff --git a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
index 370782b..1a91181 100644
--- a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
@@ -60,7 +60,7 @@
         final FileHeader header = new FileHeader(headerSize,
                 new FusionDictionary.DictionaryOptions(attributes),
                 new FormatOptions(version, FileHeader.ATTRIBUTE_VALUE_TRUE.equals(
-                        attributes.get(FileHeader.HAS_HISTORICAL_INFO_ATTRIBUTE))));
+                        attributes.get(FileHeader.HAS_HISTORICAL_INFO_KEY))));
         return header;
     }
 
diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
index 61c17fc..74e3059 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -336,16 +336,19 @@
         public final int mBodyOffset;
         public final DictionaryOptions mDictionaryOptions;
         public final FormatOptions mFormatOptions;
+
         // Note that these are corresponding definitions in native code in latinime::HeaderPolicy
         // and latinime::HeaderReadWriteUtils.
-        public static final String USES_FORGETTING_CURVE_ATTRIBUTE = "USES_FORGETTING_CURVE";
-        public static final String HAS_HISTORICAL_INFO_ATTRIBUTE = "HAS_HISTORICAL_INFO";
+        // TODO: Standardize the key names and bump up the format version, taking care not to
+        // break format version 2 dictionaries.
+        public static final String DICTIONARY_VERSION_KEY = "version";
+        public static final String DICTIONARY_LOCALE_KEY = "locale";
+        public static final String DICTIONARY_ID_KEY = "dictionary";
+        public static final String DICTIONARY_DESCRIPTION_KEY = "description";
+        public static final String DICTIONARY_DATE_KEY = "date";
+        public static final String HAS_HISTORICAL_INFO_KEY = "HAS_HISTORICAL_INFO";
+        public static final String USES_FORGETTING_CURVE_KEY = "USES_FORGETTING_CURVE";
         public static final String ATTRIBUTE_VALUE_TRUE = "1";
-
-        public static final String DICTIONARY_VERSION_ATTRIBUTE = "version";
-        public static final String DICTIONARY_LOCALE_ATTRIBUTE = "locale";
-        public static final String DICTIONARY_ID_ATTRIBUTE = "dictionary";
-        private static final String DICTIONARY_DESCRIPTION_ATTRIBUTE = "description";
         public FileHeader(final int headerSize, final DictionaryOptions dictionaryOptions,
                 final FormatOptions formatOptions) throws UnsupportedFormatException {
             mDictionaryOptions = dictionaryOptions;
@@ -365,24 +368,24 @@
 
         // Helper method to get the locale as a String
         public String getLocaleString() {
-            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_LOCALE_ATTRIBUTE);
+            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_LOCALE_KEY);
         }
 
         // Helper method to get the version String
         public String getVersion() {
-            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_VERSION_ATTRIBUTE);
+            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_VERSION_KEY);
         }
 
         // Helper method to get the dictionary ID as a String
         public String getId() {
-            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_ID_ATTRIBUTE);
+            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_ID_KEY);
         }
 
         // Helper method to get the description
         public String getDescription() {
             // TODO: Right now each dictionary file comes with a description in its own language.
             // It will display as is no matter the device's locale. It should be internationalized.
-            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_DESCRIPTION_ATTRIBUTE);
+            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_DESCRIPTION_KEY);
         }
     }
 
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
index d34aa17..b12f79b 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
@@ -62,7 +62,7 @@
         final BinaryDictionary binaryDict = new BinaryDictionary(mDictPlacedDir.getAbsolutePath(),
                 0l, mDictPlacedDir.length(), true /* useFullEditDistance */,
                 LocaleUtils.constructLocaleFromString(dict.mOptions.mAttributes.get(
-                        FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE)),
+                        FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY)),
                 Dictionary.TYPE_USER /* Dictionary type. Does not matter for us */,
                 true /* isUpdatable */);
         if (!binaryDict.isValidDictionary()) {
diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
index 4a610e6..d636a25 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
@@ -95,13 +95,13 @@
     @Override
     protected Map<String, String> getHeaderAttributeMap() {
         HashMap<String, String> attributeMap = new HashMap<String, String>();
-        attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_ATTRIBUTE,
+        attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_KEY,
                 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
-        attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_ATTRIBUTE,
+        attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_KEY,
                 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mDictName);
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale.toString());
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE,
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_KEY, mDictName);
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY, mLocale.toString());
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_KEY,
                 String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
         return attributeMap;
     }
diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
index 8ba32ff..70cb2b2 100644
--- a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
+++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
@@ -19,6 +19,7 @@
 import android.content.res.Resources;
 
 import com.android.inputmethod.keyboard.internal.KeySpecParser;
+import com.android.inputmethod.keyboard.internal.MoreKeySpec;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.Dictionary;
 import com.android.inputmethod.latin.R;
@@ -55,7 +56,7 @@
                 res.getString(R.string.symbols_word_connectors));
         mSortedWordSeparators = StringUtils.toSortedCodePointArray(
                 res.getString(R.string.symbols_word_separators));
-        final String[] suggestPuncsSpec = KeySpecParser.splitKeySpecs(res.getString(
+        final String[] suggestPuncsSpec = MoreKeySpec.splitKeySpecs(res.getString(
                 R.string.suggested_punctuations));
         mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
         mSentenceSeparator = res.getInteger(R.integer.sentence_separator);
diff --git a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
index 36b927e..b18a1d8 100644
--- a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.latin.utils;
 
 import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.Constants;
 
 import java.util.ArrayList;
 
@@ -57,9 +58,9 @@
 
     // Note that none of these characters match high or low surrogate characters, so we need not
     // take care of matching by code point.
-    private static final char COMMA = ',';
-    private static final char SPACE = ' ';
-    private static final char QUOTE = '"';
+    private static final char COMMA = Constants.CODE_COMMA;
+    private static final char SPACE = Constants.CODE_SPACE;
+    private static final char QUOTE = Constants.CODE_DOUBLE_QUOTE;
 
     @SuppressWarnings("serial")
     public static class CsvParseException extends RuntimeException {
diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
index a6ae640..7af03da 100644
--- a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
@@ -22,6 +22,7 @@
 import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
 import com.android.inputmethod.latin.makedict.DictDecoder;
 import com.android.inputmethod.latin.makedict.DictEncoder;
+import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
@@ -44,9 +45,6 @@
 public final class UserHistoryDictIOUtils {
     private static final String TAG = UserHistoryDictIOUtils.class.getSimpleName();
     private static final boolean DEBUG = false;
-    private static final String USES_FORGETTING_CURVE_KEY = "USES_FORGETTING_CURVE";
-    private static final String USES_FORGETTING_CURVE_VALUE = "1";
-    private static final String DATE_KEY = "date";
 
     public interface OnAddWordListener {
         /**
@@ -75,8 +73,9 @@
             final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams,
             final FormatOptions formatOptions, final HashMap<String, String> options) {
         final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams, options);
-        fusionDict.addOptionAttribute(USES_FORGETTING_CURVE_KEY, USES_FORGETTING_CURVE_VALUE);
-        fusionDict.addOptionAttribute(DATE_KEY,
+        fusionDict.addOptionAttribute(FormatSpec.FileHeader.USES_FORGETTING_CURVE_KEY,
+            FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
+        fusionDict.addOptionAttribute(FormatSpec.FileHeader.DICTIONARY_DATE_KEY,
                 String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
         try {
             dictEncoder.writeDictionary(fusionDict, formatOptions);
diff --git a/native/jni/src/suggest/core/dictionary/word_property.cpp b/native/jni/src/suggest/core/dictionary/word_property.cpp
index ed32bde..4a260a9 100644
--- a/native/jni/src/suggest/core/dictionary/word_property.cpp
+++ b/native/jni/src/suggest/core/dictionary/word_property.cpp
@@ -33,15 +33,16 @@
     jmethodID intToIntegerConstructorId = env->GetMethodID(integerClass, "<init>", "(I)V");
     jclass arrayListClass = env->FindClass("java/util/ArrayList");
     jmethodID addMethodId = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
-    const int shortcutTargetCount = mShortcutTargets.size();
+    const int shortcutTargetCount = mShortcuts.size();
     for (int i = 0; i < shortcutTargetCount; ++i) {
-        jintArray shortcutTargetCodePointArray = env->NewIntArray(mShortcutTargets[i].size());
+        const std::vector<int> *const targetCodePoints = mShortcuts[i].getTargetCodePoints();
+        jintArray shortcutTargetCodePointArray = env->NewIntArray(targetCodePoints->size());
         env->SetIntArrayRegion(shortcutTargetCodePointArray, 0 /* start */,
-                mShortcutTargets[i].size(), &mShortcutTargets[i][0]);
+                targetCodePoints->size(), &targetCodePoints->at(0));
         env->CallVoidMethod(outShortcutTargets, addMethodId, shortcutTargetCodePointArray);
         env->DeleteLocalRef(shortcutTargetCodePointArray);
         jobject integerProbability = env->NewObject(integerClass, intToIntegerConstructorId,
-                mShortcutProbabilities[i]);
+                mShortcuts[i].getProbability());
         env->CallVoidMethod(outShortcutProbabilities, addMethodId, integerProbability);
         env->DeleteLocalRef(integerProbability);
     }
diff --git a/native/jni/src/suggest/core/dictionary/word_property.h b/native/jni/src/suggest/core/dictionary/word_property.h
index dcac853..69c8808 100644
--- a/native/jni/src/suggest/core/dictionary/word_property.h
+++ b/native/jni/src/suggest/core/dictionary/word_property.h
@@ -28,23 +28,54 @@
 // This class is used for returning information belonging to a word to java side.
 class WordProperty {
  public:
-    // TODO: Add bigram information.
+    class BigramProperty {
+     public:
+        BigramProperty(const std::vector<int> *const targetCodePoints,
+                const int probability, const int timestamp, const int level, const int count)
+                : mTargetCodePoints(*targetCodePoints), mProbability(probability),
+                  mTimestamp(timestamp), mLevel(level), mCount(count) {}
+
+     private:
+        std::vector<int> mTargetCodePoints;
+        int mProbability;
+        int mTimestamp;
+        int mLevel;
+        int mCount;
+    };
+
+    class ShortcutProperty {
+     public:
+        ShortcutProperty(const std::vector<int> *const targetCodePoints, const int probability)
+                : mTargetCodePoints(*targetCodePoints), mProbability(probability) {}
+
+        const std::vector<int> *getTargetCodePoints() const {
+            return &mTargetCodePoints;
+        }
+
+        int getProbability() const {
+            return mProbability;
+        }
+
+     private:
+        std::vector<int> mTargetCodePoints;
+        int mProbability;
+    };
+
     // Invalid word.
     WordProperty()
             : mCodePoints(), mIsNotAWord(false), mIsBlacklisted(false),
               mHasBigrams(false), mHasShortcuts(false), mProbability(NOT_A_PROBABILITY),
-              mTimestamp(0), mLevel(0), mCount(0), mShortcutTargets(), mShortcutProbabilities() {}
+              mTimestamp(0), mLevel(0), mCount(0), mBigrams(), mShortcuts() {}
 
     WordProperty(const std::vector<int> *const codePoints,
             const bool isNotAWord, const bool isBlacklisted, const bool hasBigrams,
             const bool hasShortcuts, const int probability, const int timestamp,
-            const int level, const int count,
-            const std::vector<std::vector<int> > *const shortcutTargets,
-            const std::vector<int> *const shortcutProbabilities)
+            const int level, const int count, const std::vector<BigramProperty> *const bigrams,
+            const std::vector<ShortcutProperty> *const shortcuts)
             : mCodePoints(*codePoints), mIsNotAWord(isNotAWord), mIsBlacklisted(isBlacklisted),
               mHasBigrams(hasBigrams), mHasShortcuts(hasShortcuts), mProbability(probability),
-              mTimestamp(timestamp), mLevel(level), mCount(count),
-              mShortcutTargets(*shortcutTargets), mShortcutProbabilities(*shortcutProbabilities) {}
+              mTimestamp(timestamp), mLevel(level), mCount(count), mBigrams(*bigrams),
+              mShortcuts(*shortcuts) {}
 
     void outputProperties(JNIEnv *const env, jintArray outCodePoints, jbooleanArray outFlags,
             jintArray outProbability, jintArray outHistoricalInfo, jobject outShortcutTargets,
@@ -63,9 +94,8 @@
     int mTimestamp;
     int mLevel;
     int mCount;
-    // Shortcut
-    std::vector<std::vector<int> > mShortcutTargets;
-    std::vector<int> mShortcutProbabilities;
+    std::vector<BigramProperty> mBigrams;
+    std::vector<ShortcutProperty> mShortcuts;
 };
 } // namespace latinime
 #endif // LATINIME_WORD_PROPERTY_H
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
index 0b067e1..625b534 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
@@ -332,9 +332,10 @@
             mBuffers.get()->getProbabilityDictContent()->getProbabilityEntry(
                     ptNodeParams.getTerminalId());
     const HistoricalInfo *const historicalInfo = probabilityEntry.getHistoricalInfo();
+    // TODO: Fetch bigram information.
+    std::vector<WordProperty::BigramProperty> bigrams;
     // Fetch shortcut information.
-    std::vector<std::vector<int> > shortcutTargets;
-    std::vector<int> shortcutProbabilities;
+    std::vector<WordProperty::ShortcutProperty> shortcuts;
     int shortcutPos = getShortcutPositionOfPtNode(ptNodePos);
     if (shortcutPos != NOT_A_DICT_POS) {
         int shortcutTarget[MAX_WORD_LENGTH];
@@ -347,15 +348,14 @@
             shortcutDictContent->getShortcutEntryAndAdvancePosition(MAX_WORD_LENGTH, shortcutTarget,
                     &shortcutTargetLength, &shortcutProbability, &hasNext, &shortcutPos);
             std::vector<int> target(shortcutTarget, shortcutTarget + shortcutTargetLength);
-            shortcutTargets.push_back(target);
-            shortcutProbabilities.push_back(shortcutProbability);
+            shortcuts.push_back(WordProperty::ShortcutProperty(&target, shortcutProbability));
         }
     }
     return WordProperty(&codePointVector, ptNodeParams.isNotAWord(),
             ptNodeParams.isBlacklisted(), ptNodeParams.hasBigrams(),
             ptNodeParams.hasShortcutTargets(), ptNodeParams.getProbability(),
             historicalInfo->getTimeStamp(), historicalInfo->getLevel(),
-            historicalInfo->getCount(), &shortcutTargets, &shortcutProbabilities);
+            historicalInfo->getCount(), &bigrams, &shortcuts);
 }
 
 } // namespace latinime
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
similarity index 97%
rename from tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java
rename to tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
index cbe2d59..5f301a8 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
@@ -31,7 +31,7 @@
 import java.util.Locale;
 
 @MediumTest
-public class KeySpecParserSplitTests extends InstrumentationTestCase {
+public class MoreKeySpecSplitTests extends InstrumentationTestCase {
     private static final Locale TEST_LOCALE = Locale.ENGLISH;
     final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
 
@@ -92,8 +92,8 @@
 
     private void assertTextArray(final String message, final String value,
             final String ... expectedArray) {
-        final String resolvedActual = KeySpecParser.resolveTextReference(value, mTextsSet);
-        final String[] actual = KeySpecParser.splitKeySpecs(resolvedActual);
+        final String resolvedActual = mTextsSet.resolveTextReference(value);
+        final String[] actual = MoreKeySpec.splitKeySpecs(resolvedActual);
         final String[] expected = (expectedArray.length == 0) ? null : expectedArray;
         assertArrayEquals(message, expected, actual);
     }
@@ -117,13 +117,11 @@
     private static final String SURROGATE2 = PAIR1 + PAIR2 + PAIR3;
 
     public void testResolveNullText() {
-        assertNull("resolve null", KeySpecParser.resolveTextReference(
-                null, mTextsSet));
+        assertNull("resolve null", mTextsSet.resolveTextReference(null));
     }
 
     public void testResolveEmptyText() {
-        assertNull("resolve empty text", KeySpecParser.resolveTextReference(
-                "!text/empty_string", mTextsSet));
+        assertNull("resolve empty text", mTextsSet.resolveTextReference("!text/empty_string"));
     }
 
     public void testSplitZero() {
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecTests.java
similarity index 95%
rename from tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
rename to tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecTests.java
index afb2b03..538ba2c 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecTests.java
@@ -32,7 +32,7 @@
 import java.util.Locale;
 
 @SmallTest
-public class KeySpecParserTests extends AndroidTestCase {
+public class MoreKeySpecTests extends AndroidTestCase {
     private final static Locale TEST_LOCALE = Locale.ENGLISH;
     final KeyboardCodesSet mCodesSet = new KeyboardCodesSet();
     final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
@@ -71,9 +71,10 @@
         mSettingsIconId = KeySpecParser.getIconId(ICON_SETTINGS);
     }
 
-    private void assertParser(String message, String moreKeySpec, String expectedLabel,
-            String expectedOutputText, int expectedIcon, int expectedCode) {
-        final String labelResolved = KeySpecParser.resolveTextReference(moreKeySpec, mTextsSet);
+    private void assertParser(final String message, final String moreKeySpec,
+            final String expectedLabel, final String expectedOutputText, final int expectedIcon,
+            final int expectedCode) {
+        final String labelResolved = mTextsSet.resolveTextReference(moreKeySpec);
         final MoreKeySpec spec = new MoreKeySpec(labelResolved, false /* needsToUpperCase */,
                 Locale.US, mCodesSet);
         assertEquals(message + " [label]", expectedLabel, spec.mLabel);
@@ -86,8 +87,9 @@
                 Constants.printableCode(spec.mCode));
     }
 
-    private void assertParserError(String message, String moreKeySpec, String expectedLabel,
-            String expectedOutputText, int expectedIcon, int expectedCode) {
+    private void assertParserError(final String message, final String moreKeySpec,
+            final String expectedLabel, final String expectedOutputText, final int expectedIcon,
+            final int expectedCode) {
         try {
             assertParser(message, moreKeySpec, expectedLabel, expectedOutputText, expectedIcon,
                     expectedCode);
@@ -339,7 +341,8 @@
                 null, null, mSettingsIconId, mCodeSettings);
     }
 
-    private static void assertArrayEquals(String message, Object[] expected, Object[] actual) {
+    private static void assertArrayEquals(final String message, final Object[] expected,
+            final Object[] actual) {
         if (expected == actual) {
             return;
         }
@@ -357,10 +360,9 @@
         }
     }
 
-    private static void assertInsertAdditionalMoreKeys(String message, String[] moreKeys,
-            String[] additionalMoreKeys, String[] expected) {
-        final String[] actual =
-                KeySpecParser.insertAdditionalMoreKeys( moreKeys, additionalMoreKeys);
+    private static void assertInsertAdditionalMoreKeys(final String message,
+            final String[] moreKeys, final String[] additionalMoreKeys, final String[] expected) {
+        final String[] actual = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
         assertArrayEquals(message, expected, actual);
     }
 
@@ -584,10 +586,10 @@
     private static final String AUTO_COLUMN_ORDER = "!autoColumnOrder!";
     private static final String FIXED_COLUMN_ORDER = "!fixedColumnOrder!";
 
-    private static void assertGetBooleanValue(String message, String key, String[] moreKeys,
-            String[] expected, boolean expectedValue) {
+    private static void assertGetBooleanValue(final String message, final String key,
+            final String[] moreKeys, final String[] expected, final boolean expectedValue) {
         final String[] actual = Arrays.copyOf(moreKeys, moreKeys.length);
-        final boolean actualValue = KeySpecParser.getBooleanValue(actual, key);
+        final boolean actualValue = MoreKeySpec.getBooleanValue(actual, key);
         assertEquals(message + " [value]", expectedValue, actualValue);
         assertArrayEquals(message, expected, actual);
     }
@@ -622,10 +624,11 @@
                     "a", null, "b", NEEDS_DIVIDER, "!HASLABEL!", "d" }, true);
     }
 
-    private static void assertGetIntValue(String message, String key, int defaultValue,
-            String[] moreKeys, String[] expected, int expectedValue) {
+    private static void assertGetIntValue(final String message, final String key,
+            final int defaultValue, final String[] moreKeys, final String[] expected,
+            final int expectedValue) {
         final String[] actual = Arrays.copyOf(moreKeys, moreKeys.length);
-        final int actualValue = KeySpecParser.getIntValue(actual, key, defaultValue);
+        final int actualValue = MoreKeySpec.getIntValue(actual, key, defaultValue);
         assertEquals(message + " [value]", expectedValue, actualValue);
         assertArrayEquals(message, expected, actual);
     }
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
index c427656..343ab42 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
@@ -102,13 +102,13 @@
                 getContext().getCacheDir());
         FileUtils.deleteRecursively(file);
         Map<String, String> attributeMap = new HashMap<String, String>();
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, dictId);
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, dictId);
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE,
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_KEY, dictId);
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY, dictId);
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_KEY,
                 String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
-        attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_ATTRIBUTE,
+        attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_KEY,
                 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
-        attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_ATTRIBUTE,
+        attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_KEY,
                 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
         if (BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(),
                 FormatSpec.VERSION4, attributeMap)) {
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
index 8a1ac52..715db2f 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
@@ -108,6 +108,19 @@
         }
     }
 
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        BinaryDictionary.setCurrentTimeForTest(0);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        // Quit test mode.
+        BinaryDictionary.setCurrentTimeForTest(-1);
+    }
+
     private void generateWords(final int number, final Random random) {
         final int[] codePointSet = CodePointUtils.generateCodePointSet(DEFAULT_CODE_POINT_SET_SIZE,
                 random);
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictUtils.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictUtils.java
index f175968..20cf9a5 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictUtils.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictUtils.java
@@ -39,13 +39,13 @@
     public static DictionaryOptions makeDictionaryOptions(final String id, final String version,
             final FormatSpec.FormatOptions formatOptions) {
         final DictionaryOptions options = new DictionaryOptions(new HashMap<String, String>());
-        options.mAttributes.put(FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, "en_US");
-        options.mAttributes.put(FileHeader.DICTIONARY_ID_ATTRIBUTE, id);
-        options.mAttributes.put(FileHeader.DICTIONARY_VERSION_ATTRIBUTE, version);
+        options.mAttributes.put(FileHeader.DICTIONARY_LOCALE_KEY, "en_US");
+        options.mAttributes.put(FileHeader.DICTIONARY_ID_KEY, id);
+        options.mAttributes.put(FileHeader.DICTIONARY_VERSION_KEY, version);
         if (formatOptions.mHasTimestamp) {
-            options.mAttributes.put(FileHeader.HAS_HISTORICAL_INFO_ATTRIBUTE,
+            options.mAttributes.put(FileHeader.HAS_HISTORICAL_INFO_KEY,
                     FileHeader.ATTRIBUTE_VALUE_TRUE);
-            options.mAttributes.put(FileHeader.USES_FORGETTING_CURVE_ATTRIBUTE,
+            options.mAttributes.put(FileHeader.USES_FORGETTING_CURVE_KEY,
                     FileHeader.ATTRIBUTE_VALUE_TRUE);
         }
         return options;
diff --git a/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
index ae08b49..93731b3 100644
--- a/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
@@ -56,9 +56,9 @@
     private static final String TEST_DICT_FILE_EXTENSION = ".testDict";
     private static final HashMap<String, String> HEADER_OPTIONS = new HashMap<String, String>();
     static {
-        HEADER_OPTIONS.put(FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, "en_US");
-        HEADER_OPTIONS.put(FileHeader.DICTIONARY_ID_ATTRIBUTE, "test");
-        HEADER_OPTIONS.put(FileHeader.DICTIONARY_VERSION_ATTRIBUTE, "1000");
+        HEADER_OPTIONS.put(FileHeader.DICTIONARY_LOCALE_KEY, "en_US");
+        HEADER_OPTIONS.put(FileHeader.DICTIONARY_ID_KEY, "test");
+        HEADER_OPTIONS.put(FileHeader.DICTIONARY_VERSION_KEY, "1000");
     }
 
     /**
diff --git a/tools/dicttool/Android.mk b/tools/dicttool/Android.mk
index b7b1fe6..adfb920 100644
--- a/tools/dicttool/Android.mk
+++ b/tools/dicttool/Android.mk
@@ -49,7 +49,7 @@
         $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/LocaleUtils.java \
         $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/ResizableIntArray.java \
         $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/StringUtils.java \
-        $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/UnigramProperty.java
+        $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/WordProperty.java
 
 DICTTOOL_ONDEVICE_TESTS_DIRECTORY := \
         $(LATINIME_LOCAL_DIR)/tests/src/com/android/inputmethod/latin/makedict/
diff --git a/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java b/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
index 0c11f86..d8059e4 100644
--- a/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
+++ b/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
@@ -48,9 +48,9 @@
 
         // Create a thrice-compressed dictionary file.
         final DictionaryOptions testOptions = new DictionaryOptions(new HashMap<String, String>());
-        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE, VERSION);
-        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, LOCALE);
-        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, ID);
+        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_VERSION_KEY, VERSION);
+        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY, LOCALE);
+        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_ID_KEY, ID);
         final FusionDictionary dict = new FusionDictionary(new PtNodeArray(), testOptions);
         dict.add("foo", TEST_FREQ, null, false /* isNotAWord */);
         dict.add("fta", 1, null, false /* isNotAWord */);
@@ -80,11 +80,11 @@
                 null /* dict : an optional dictionary to add words to, or null */,
                 false /* deleteDictIfBroken */);
         assertEquals("Wrong version attribute", VERSION, resultDict.mOptions.mAttributes.get(
-                FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE));
+                FormatSpec.FileHeader.DICTIONARY_VERSION_KEY));
         assertEquals("Wrong locale attribute", LOCALE, resultDict.mOptions.mAttributes.get(
-                FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE));
+                FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY));
         assertEquals("Wrong id attribute", ID, resultDict.mOptions.mAttributes.get(
-                FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE));
+                FormatSpec.FileHeader.DICTIONARY_ID_KEY));
         assertEquals("Dictionary can't be read back correctly",
                 FusionDictionary.findWordInTree(resultDict.mRootNodeArray, "foo").getFrequency(),
                 TEST_FREQ);
diff --git a/tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl b/tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
index a8ac981..db1dde3 100644
--- a/tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
+++ b/tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
@@ -18,8 +18,10 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.text.TextUtils;
 
 import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.util.HashMap;
@@ -45,6 +47,10 @@
  *   KeyboardTextsSet.java
  */
 public final class KeyboardTextsSet {
+    public static final String PREFIX_TEXT = "!text/";
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
+    private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
+
     // Language to texts map.
     private static final HashMap<String, String[]> sLocaleToTextsMap = CollectionUtils.newHashMap();
     private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap();
@@ -87,6 +93,67 @@
         return (text == null) ? LANGUAGE_DEFAULT[id] : text;
     }
 
+    private static int searchTextNameEnd(final String text, final int start) {
+        final int size = text.length();
+        for (int pos = start; pos < size; pos++) {
+            final char c = text.charAt(pos);
+            // Label name should be consisted of [a-zA-Z_0-9].
+            if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) {
+                continue;
+            }
+            return pos;
+        }
+        return size;
+    }
+
+    public String resolveTextReference(final String rawText) {
+        if (TextUtils.isEmpty(rawText)) {
+            return null;
+        }
+        int level = 0;
+        String text = rawText;
+        StringBuilder sb;
+        do {
+            level++;
+            if (level >= MAX_STRING_REFERENCE_INDIRECTION) {
+                throw new RuntimeException("too many @string/resource indirection: " + text);
+            }
+
+            final int prefixLen = PREFIX_TEXT.length();
+            final int size = text.length();
+            if (size < prefixLen) {
+                return TextUtils.isEmpty(text) ? null : text;
+            }
+
+            sb = null;
+            for (int pos = 0; pos < size; pos++) {
+                final char c = text.charAt(pos);
+                if (text.startsWith(PREFIX_TEXT, pos)) {
+                    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;
+                } else if (c == BACKSLASH) {
+                    if (sb != null) {
+                        // Append both escape character and escaped character.
+                        sb.append(text.substring(pos, Math.min(pos + 2, size)));
+                    }
+                    pos++;
+                } else if (sb != null) {
+                    sb.append(c);
+                }
+            }
+
+            if (sb != null) {
+                text = sb.toString();
+            }
+        } while (sb != null);
+        return TextUtils.isEmpty(text) ? null : text;
+    }
+
     // These texts' name should be aligned with the @string/<name> in
     // values*/strings-action-keys.xml.
     private static final String[] RESOURCE_NAMES = {