1

For the sake of this question, imagine my application helps users to practice foreign languages.

They press a button to start the Text to Speech introduction, which speaks:

<string name="repeat_after_me" translatable="true">Repeat after me</string>

This string will be localised in the normal way, fetching the string from the appropriate res/values-lang/strings.xml file according to the device Locale.

After the introduction, the app needs to speak any one of a random number of strings, in the language/locale of which they are currently wishing to learn. Herein lies the problem.

Assuming the Text to Speech starts from a simple method such as:

private void startLearning(Locale learningLocale)

And the pseudo code of:

TTS.speak(getString(R.string.repeat_after_me)

followed by:

TTS.speak(getRandomLearningString(learningLocale))

Where:

String getRandomLearningString(Locale learningLocale) {
// return a random string here
}

The above is where I'm stuck on how to best reference the xml resource, that contains the 'string-array' of the language the user is learning (in order to pick one at random).

<string-array name="en_EN" translatable="false">
    <item>"Where is the nearest hospital?"</item>
    <item>"What's the time please?"</item>
    <item>"Only if you promise to wear protection and we have a safe word"</item>
</string-array>

Assuming I have a large number of strings for each language and I support a vast number of languages, the question:

How should I store these strings to keep them manageable and readable in development? How should I 'dynamically' reference them from a method?

To clarify - the main problem is not only how I resolve:

getStringArray(R.array.(variableLocale);

But also how/where I store these string arrays so that the implementation is scalable and organised.

I thank you in advance.

Edit - The actually Text to Speech implementation of switching languages is not a problem, I have that covered.

brandall
  • 6,094
  • 4
  • 49
  • 103

2 Answers2

1

Scalebale Solution

If you want to keep this scaleble, you need to save your strings in a form which supports random access without loading everything into memory. So, a plain file (which strings.xml is essentially) won't do the job.

I recommend you check if you can accomplish what you want with an SQLite Database.

This would result in something like:

SELECT text FROM table WHERE locale = yourlocale ORDER BY RANDOM() LIMIT 1

(see Select random row from an sqlite table).


This solution requires quite a lot of work to create the needed database, so for known small situations, use the solution below.

Limited solution

If you know you won't have too many entries I would recommend to use plain textfiles (one per language). They are easiest to manage.

You can either save them as raw resource or in the assets folder. Both are relatively easy to read into a String. Then you just need to call String.split("\n") and have an array from which you can select one at random.

Alternatively, you can put the strings in a string-array in each localized strings.xml and load the wanted array using resources like this:

Resources standardResources = context.getResources();
AssetManager assets = standardResources.getAssets();
DisplayMetrics metrics = standardResources.getDisplayMetrics();
Configuration config = new Configuration(standardResources.getConfiguration());
config.locale = yourLocale;
Resources resources = new Resources(assets, metrics, config);

(see : Load language specific string from resource?)

As noted in the sources comments this seems to override the resources returned from context.getResources(), maybe you have to reset to the previous locale afterwards.

Starting from Jellybean there is also context.createConfigurationContext, which doesn't seem to have this problem.

In all cases it might be a good idea to cache the array if you need to select entries repeatedly.


Note: This solution doesn't scale well, because the whole array has to be loaded into memory just to select one entry. So large collections might exceed your heap or at least use a lot of memory.

Community
  • 1
  • 1
F43nd1r
  • 7,690
  • 3
  • 24
  • 62
  • I appreciate you taking the time to answer, but I have doubts that reading an xml array (of let's say 250 entries) is significantly less efficient than reading from a database, enough to warrant such a 'design change'? I do of course agree that it would be very simple to query. – brandall Mar 25 '16 at 04:23
  • you asked for a scalable solution. Of course you can use a file **as long as** reading it doesn't take more than a split second. This solution will work performant with (almost) any amount of entries. – F43nd1r Mar 25 '16 at 04:26
  • Understood and agreed. With the Android string xml translation and locale options, I was very much hoping there would be a more 'out of the box' solution. – brandall Mar 25 '16 at 04:34
  • edited to provide less efficient yet easier solutions, they should fit better to your usecase. – F43nd1r Mar 25 '16 at 04:50
  • Can you clarify please `everything has to be loaded into memory just to select one. So large collections might exceed your heap` You are suggesting this happens every time you call `getResources()`? From your experience, what is the size limit of resources that could cause this failure and does aapt not prevent compilation of resources that could potentially cause this? Also, why do you suggest a plain text file in raw/assets and splitting strings ahead of an xml array? – brandall Mar 31 '16 at 03:44
  • getResources doesn't load anything. I've clarified the sentence. I have never actually exceeded my heap with strings (altough loading 100.000 characters has caused an onLowMemory in an already busy application). I've done it with 50 144*144 Bitmaps in an otherwise idle application though, maybe that can serve as a scale. But this differs depending on the heap size anyway. – F43nd1r Mar 31 '16 at 13:38
  • `Also, why do you suggest a plain text file in raw/assets and splitting strings ahead of an xml array?` Historical reasons, I added the xml array last. Also plain files are easier to import, export & edit. – F43nd1r Mar 31 '16 at 13:41
  • Thank you. If you add the `createConfigurationContext()` from my answer below, to yours (for reference), I'll mark your answer as correct. – brandall Mar 31 '16 at 13:46
  • Done. Why did you create the ResourceManager class? It seems to call only super anyway. – F43nd1r Mar 31 '16 at 13:54
  • Thanks - I needed to handle further issues of potentially missing resources, so left it in there just in case others may need to too. Otherwise, yes, it's useless. – brandall Mar 31 '16 at 13:57
0

Taking reference from this answer and this answer, I came up with the following custom class solution:

package com.my.package.localisation;

import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.support.annotation.NonNull;
import android.util.DisplayMetrics;

import java.util.Formatter;
import java.util.Locale;

/**
 * Class to manage fetching {@link Resources} for a specific {@link Locale}. API levels less
 * than {@link Build.VERSION_CODES#JELLY_BEAN_MR1} require an ugly implementation.
 * <p/>
 * Subclass extends {@link Resources} in case of further functionality requirements.
 */
public class MyResources {

    private final Context mContext;
    private final AssetManager assetManager;
    private final DisplayMetrics metrics;
    private final Configuration configuration;
    private final Locale targetLocale;
    private final Locale defaultLocale;

    public MyResources(@NonNull final Context mContext, @NonNull final Locale defaultLocale,
                         @NonNull final Locale targetLocale) {

        this.mContext = mContext;
        final Resources resources = this.mContext.getResources();
        this.assetManager = resources.getAssets();
        this.metrics = resources.getDisplayMetrics();
        this.configuration = new Configuration(resources.getConfiguration());
        this.targetLocale = targetLocale;
        this.defaultLocale = defaultLocale;
    }

    public String[] getStringArray(final int resourceId) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            configuration.setLocale(targetLocale);
            return mContext.createConfigurationContext(configuration).getResources().getStringArray(resourceId);
        } else {
            configuration.locale = targetLocale;
            final String[] resourceArray = new ResourceManager(assetManager, metrics, configuration).getStringArray(resourceId);
            configuration.locale = defaultLocale; // reset
            new ResourceManager(assetManager, metrics, configuration); // reset
            return resourceArray;
        }
    }

    public String getString(final int resourceId) {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            configuration.setLocale(targetLocale);
            return mContext.createConfigurationContext(configuration).getResources().getString(resourceId);
        } else {
            configuration.locale = targetLocale;
            final String resource = new ResourceManager(assetManager, metrics, configuration).getString(resourceId);
            configuration.locale = defaultLocale; // reset
            new ResourceManager(assetManager, metrics, configuration); // reset
            return resource;
        }
    }

    private final class ResourceManager extends Resources {
        public ResourceManager(final AssetManager assets, final DisplayMetrics metrics, final Configuration config) {
            super(assets, metrics, config);
        }

        /**
         * Return the string array associated with a particular resource ID.
         *
         * @param id The desired resource identifier, as generated by the aapt
         *           tool. This integer encodes the package, type, and resource
         *           entry. The value 0 is an invalid identifier.
         * @return The string array associated with the resource.
         * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
         */
        @Override
        public String[] getStringArray(final int id) throws NotFoundException {
            return super.getStringArray(id);
        }

        /**
         * Return the string value associated with a particular resource ID,
         * substituting the format arguments as defined in {@link Formatter}
         * and {@link String#format}. It will be stripped of any styled text
         * information.
         * {@more}
         *
         * @param id         The desired resource identifier, as generated by the aapt
         *                   tool. This integer encodes the package, type, and resource
         *                   entry. The value 0 is an invalid identifier.
         * @param formatArgs The format arguments that will be used for substitution.
         * @return String The string data associated with the resource,
         * stripped of styled text information.
         * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
         */
        @NonNull
        @Override
        public String getString(final int id, final Object... formatArgs) throws NotFoundException {
            return super.getString(id, formatArgs);
        }

        /**
         * Return the string value associated with a particular resource ID.  It
         * will be stripped of any styled text information.
         * {@more}
         *
         * @param id The desired resource identifier, as generated by the aapt
         *           tool. This integer encodes the package, type, and resource
         *           entry. The value 0 is an invalid identifier.
         * @return String The string data associated with the resource,
         * stripped of styled text information.
         * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
         */
        @NonNull
        @Override
        public String getString(final int id) throws NotFoundException {
            return super.getString(id);
        }
    }
}
Community
  • 1
  • 1
brandall
  • 6,094
  • 4
  • 49
  • 103