On Android every translation string is defined in a strings.xml file. At compile time, aapt wil put every values resource in a resources.arsc file, after which the R.java file (containing ids) is generated. But this resources.arsc file can not be changed while running the application. Android also does not provide a way to dynamically change the translations.

But there is a way to do it:

When you start digging into the Android source code, you'll find lots of places where translation keys/ids get translated. In the end, all of them will end up in the AssetManager class. This AssetManager contains a private array mStringBlocks containing the translation values and blockIds. Other then that, the AssetManager contains a private method loadResourceValue which returns the blockId based on the resourceId. With reflection we can work some magic, turn them public, and call them. Using this method, we can now get translations from memory.

 

Based on a resId, AssetManager.loadResourceValue returns an index to get the correct StringBlock from the mStringBlocks array. The TypedValue object that is passed into the AssetManager.loadResourceValue method is updated with a data property which is the index of the SparseString within that StringBlock’s mSparseStrings array. With these 2 indexes, we have what’s needed for getting and setting the translated String value.

private TranslationLookup getTranslationFromMemory(int resId) {
    try {
        Method m = AssetManager.class.getDeclaredMethod("loadResourceValue", int.class, short.class, TypedValue.class, boolean.class);
        m.setAccessible(true);
        TypedValue outValue = new TypedValue();
        short t = (short) 0;
        Object block = m.invoke(resources.getAssets(), resId, t, outValue, true);
        Field f = FieldUtils.getField(AssetManager.class, "mStringBlocks", true);
        f.setAccessible(true);
        Object[] mStringBlocks = (Object[]) f.get(resources.getAssets());
        Field f2 = FieldUtils.getField(mStringBlocks[0].getClass(), "mSparseStrings", true);
        f2.setAccessible(true);
        SparseArray sa = (SparseArray) f2.get(mStringBlocks[(int) block]);
        outValue.string = (CharSequence) sa.get(outValue.data);
        TranslationLookup tl = new TranslationLookup(outValue, (Integer) block);
        return tl;
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    }
    return null;
}

 

If we want to live change the translation values, the only thing we have to do is load the original value, and then change the in-memory array of StringBlocks.

To wrap it up in a proof of concept, we created a live translation editor where you can view the currently used translations and change them live. And here can you find the Util class itself to get and change the translations.

Our live translation editor also adds a way to export the changes you made so they can be imported in you Android or iOS projects.

Written by

Kris Pypen

Coach mobile development

Follow @krispypen