Merge "Fix BinaryDictDecoderEncoderTests."
diff --git a/java/res/values/strings-config-important-notice.xml b/java/res/values/strings-config-important-notice.xml
new file mode 100644
index 0000000..8ee0b99
--- /dev/null
+++ b/java/res/values/strings-config-important-notice.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<resources>
+    <integer name="config_important_notice_version">0</integer>
+    <!-- TODO: Add important notice title and description string resources. -->
+</resources>
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
index e9e2f42..07c5cae 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
@@ -48,15 +48,10 @@
 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 String PREFIX_HEX = "0x";
     private static final String ADDITIONAL_MORE_KEY_MARKER = "%";
 
@@ -118,13 +113,13 @@
     }
 
     private static boolean hasIcon(final String moreKeySpec) {
-        return moreKeySpec.startsWith(PREFIX_ICON);
+        return moreKeySpec.startsWith(KeyboardIconsSet.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)) {
+                KeyboardCodesSet.PREFIX_CODE, end + 1)) {
             return true;
         }
         return false;
@@ -246,8 +241,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 {
@@ -257,29 +252,16 @@
 
     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);
+            final int end = moreKeySpec.indexOf(
+                    VERTICAL_BAR, KeyboardIconsSet.PREFIX_ICON.length());
+            final String name = (end < 0)
+                    ? moreKeySpec.substring(KeyboardIconsSet.PREFIX_ICON.length())
+                    : moreKeySpec.substring(KeyboardIconsSet.PREFIX_ICON.length(), end);
             return KeyboardIconsSet.getIconId(name);
         }
         return KeyboardIconsSet.ICON_UNDEFINED;
     }
 
-    private static <T> ArrayList<T> arrayAsList(final T[] array, final int start, final int end) {
-        if (array == null) {
-            throw new NullPointerException();
-        }
-        if (start < 0 || start > end || end > array.length) {
-            throw new IllegalArgumentException();
-        }
-
-        final ArrayList<T> list = CollectionUtils.newArrayList(end - start);
-        for (int i = start; i < end; i++) {
-            list.add(array[i]);
-        }
-        return list;
-    }
-
     private static final String[] EMPTY_STRING_ARRAY = new String[0];
 
     private static String[] filterOutEmptyString(final String[] array) {
@@ -291,7 +273,7 @@
             final String entry = array[i];
             if (TextUtils.isEmpty(entry)) {
                 if (out == null) {
-                    out = arrayAsList(array, 0, i);
+                    out = CollectionUtils.arrayAsList(array, 0, i);
                 }
             } else if (out != null) {
                 out.add(entry);
@@ -326,7 +308,7 @@
                 } else {
                     // Filter out excessive '%' marker.
                     if (out == null) {
-                        out = arrayAsList(moreKeys, 0, moreKeyIndex);
+                        out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
                     }
                 }
             } else {
@@ -343,7 +325,7 @@
                         + " moreKeys=" + Arrays.toString(moreKeys)
                         + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
             }
-            out = arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
+            out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
             for (int i = 0; i < moreKeysCount; i++) {
                 out.add(moreKeys[i]);
             }
@@ -355,7 +337,7 @@
                         + " moreKeys=" + Arrays.toString(moreKeys)
                         + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
             }
-            out = arrayAsList(moreKeys, 0, moreKeysCount);
+            out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
             for (int i = additionalIndex; i < additionalCount; i++) {
                 out.add(additionalMoreKeys[additionalIndex]);
             }
@@ -376,68 +358,6 @@
         }
     }
 
-    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) {
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
index e6a6743..a2242b8 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
@@ -32,14 +32,14 @@
 
     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);
+            final String text = mTextsSet.resolveTextReference(a.getString(index));
             return KeySpecParser.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..96a5b08 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.text.TextUtils;
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.utils.CollectionUtils;
@@ -45,6 +46,10 @@
  *   KeyboardTextsSet.java
  */
 public final class KeyboardTextsSet {
+    public static final String PREFIX_TEXT = "!text/";
+    private static final char 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 +92,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..dd58e1f 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
@@ -74,7 +74,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) {
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/utils/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
index cc25102..bbfa0f0 100644
--- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
@@ -102,4 +102,19 @@
     public static <E> SparseArray<E> newSparseArray() {
         return new SparseArray<E>();
     }
+
+    public static <E> ArrayList<E> arrayAsList(final E[] array, final int start, final int end) {
+        if (array == null) {
+            throw new NullPointerException();
+        }
+        if (start < 0 || start > end || end > array.length) {
+            throw new IllegalArgumentException();
+        }
+
+        final ArrayList<E> list = newArrayList(end - start);
+        for (int i = start; i < end; i++) {
+            list.add(array[i]);
+        }
+        return list;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
index 8eff2e6..4a08231 100644
--- a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
@@ -22,13 +22,11 @@
 import android.provider.Settings.SettingNotFoundException;
 import android.util.Log;
 
+import com.android.inputmethod.latin.R;
+
 public final class ImportantNoticeUtils {
     private static final String TAG = ImportantNoticeUtils.class.getSimpleName();
 
-    // The current version number of an important notice.
-    // This number must be incremented whenever users should see a new important notice.
-    private static final int CURRENT_IMPORTANT_NOTICE_VERSION = 0;
-
     // {@link SharedPreferences} name to save the last important notice version that has been
     // displayed to users.
     private static final String PREFERENCE_NAME = "important_notice";
@@ -60,16 +58,20 @@
         return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
     }
 
+    private static int getCurrentImportantNoticeVersion(final Context context) {
+        return context.getResources().getInteger(R.integer.config_important_notice_version);
+    }
+
     public static boolean hasNewImportantNotice(final Context context) {
         final SharedPreferences prefs = getImportantNoticePreferences(context);
         final int lastVersion = prefs.getInt(KEY_IMPORTANT_NOTICE_VERSION, 0);
-        return CURRENT_IMPORTANT_NOTICE_VERSION > lastVersion;
+        return getCurrentImportantNoticeVersion(context) > lastVersion;
     }
 
     public static void updateLastImportantNoticeVersion(final Context context) {
         final SharedPreferences prefs = getImportantNoticePreferences(context);
         prefs.edit()
-                .putInt(KEY_IMPORTANT_NOTICE_VERSION, CURRENT_IMPORTANT_NOTICE_VERSION)
+                .putInt(KEY_IMPORTANT_NOTICE_VERSION, getCurrentImportantNoticeVersion(context))
                 .apply();
     }
 }
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/KeySpecParserSplitTests.java
index cbe2d59..7b7f175 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java
@@ -92,7 +92,7 @@
 
     private void assertTextArray(final String message, final String value,
             final String ... expectedArray) {
-        final String resolvedActual = KeySpecParser.resolveTextReference(value, mTextsSet);
+        final String resolvedActual = mTextsSet.resolveTextReference(value);
         final String[] actual = KeySpecParser.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/KeySpecParserTests.java
index afb2b03..0388435 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
@@ -73,7 +73,7 @@
 
     private void assertParser(String message, String moreKeySpec, String expectedLabel,
             String expectedOutputText, int expectedIcon, int expectedCode) {
-        final String labelResolved = KeySpecParser.resolveTextReference(moreKeySpec, mTextsSet);
+        final String labelResolved = mTextsSet.resolveTextReference(moreKeySpec);
         final MoreKeySpec spec = new MoreKeySpec(labelResolved, false /* needsToUpperCase */,
                 Locale.US, mCodesSet);
         assertEquals(message + " [label]", expectedLabel, spec.mLabel);
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/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..9252992 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,6 +18,7 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.text.TextUtils;
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.utils.CollectionUtils;
@@ -45,6 +46,10 @@
  *   KeyboardTextsSet.java
  */
 public final class KeyboardTextsSet {
+    public static final String PREFIX_TEXT = "!text/";
+    private static final char 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 +92,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 = {