Managing dynamic localisation resources

2019-07-24 16:54发布

问题:

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.

回答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.



回答2:

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);
        }
    }
}