2

Background

I know it's possible to iterate over all strings of a given R java file, using reflection as such (based on here):

    for (Field field : R.string.class.getDeclaredFields()) {
        if (Modifier.isStatic(field.getModifiers()) && !Modifier.isPrivate(field.getModifiers()) && field.getType().equals(int.class)) {
            try {
                int stringResId = field.getInt(null);
                Log.d("AppLog", field.getName() + ":" + getResources().getString(stringResId));
            } catch (IllegalArgumentException | IllegalAccessException e) {
                Log.e("AppLog", e.toString());
            }
        }
    }

This will show all of the strings used in the app, including of all modules together (they are merged), using the current locale.

The question

Is it possible to iterate over all strings of specific modules (or all except specific ones), and also iterate over them in all supported languages ?

Community
  • 1
  • 1
android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • `Configuration config = new Configuration(); for (Locale locale : Locale.getAvailableLocales()) { config.setLocale(locale); Resources res = createConfigurationContext(config).getResources(); Log.d(TAG, "onClick " + locale.getDisplayName() + ": " + res.getString(R.string.button)); }` – pskink Mar 15 '17 at 11:37
  • @pskink Will it go only over the locales that are used in the app, or will it go over all device-supported languages ? Also, why "onClick " ? :) – android developer Mar 15 '17 at 11:44
  • because i run tho code inside onClick method and have a macro that defines automatically Log.d statement with the current method name – pskink Mar 15 '17 at 11:48
  • @pskink OK, what about the question I asked about it? – android developer Mar 15 '17 at 12:11
  • i dont know what you mean – pskink Mar 15 '17 at 12:13
  • @pskink I mean this question: "Will it go only over the locales that are used in the app, or will it go over all device-supported languages ?" – android developer Mar 15 '17 at 12:26

2 Answers2

1

Is it possible to iterate over all strings of specific modules (or all except specific ones

No. There's no concept of "module" for resources.

As you already noticed these are all merged into one R. So your only chance is to make keys unique (i.e. prefixed) and then skip all keys that are not prefixed during your walk, or you can create separate elements holding all the keys of strings from given module and work on it later, which should be faster than iterating over all, still painful to maintain. Or you can combine both, and build such filtered array based on prefix on startup and then work on it later if you need to access it more times.

EDIT

For example, iterate over both English and German strings ?

First, note that for most entries you will have the same keys (which obvious, but easy to overlook). And to iterate on both, you need to get the right resource context:

Resources rsrc = getResources();
Configuration config = new Configuration(rsrc.getConfiguration());
config.locale = Locale.US;   //
Resources localeResources = new Resources(rsrc.getAssets(), rsrc.getDisplayMetrics(), config);

and then read as usual. And then repeat setting Locale.GERMAN in config.

EDIT 2

also iterate over them in all supported languages

Theoretically AssetManager features getLocales():

Get the locales that this asset manager contains data for.

but things get tricky here when you realise that "contains data for" is not what you mean. It's sufficient to include i.e. external library which provides i.e. Greek translation too, to have Greek language also returned.

So the simplest approach may simply be to keep index of supported languages as part of your code be it directly in code or maybe as array in resources and rely on that whenever you want to know what languages your app support. This should be easy to maintain and you can even create small gradle task to have you this generated at build time.

Marcin Orlowski
  • 72,056
  • 11
  • 123
  • 141
  • I see. But how come there are multiple R.java files, then? Can't I somehow find them and iterate over them instead of the merged one? Also, what about the language iteration, without the need to switch over device locales? For example, iterate over both English and German strings ? – android developer Mar 15 '17 at 11:07
  • So instead of "getResources()" I will use the one you called "defaultResources" ? – android developer Mar 15 '17 at 12:11
  • Yes, that will be using your config so pointing to strings of language of your choice. I renamed it to `localeResources` as it makes more sense – Marcin Orlowski Mar 15 '17 at 12:11
  • I see. Is it possible to iterate over the locales that are supported on the app, or do I have to go over all of them that are supported on the device itself? Also, What about the other questions? Isn't there a way to check the other R files? – android developer Mar 15 '17 at 12:13
  • As for iterating over locale supported by the app - I think that should be another question. There are many `R`s during building, yet finally there're merged into one. That's why it is advised to prefix own resource keys when you create library you want to share with others as if you won't do that then there's high chance your keys (esp. generic) will clash with app's ones. Also note that `R` is not the same as value it references (that's why you still can have many languages, despite having just one `R`). – Marcin Orlowski Mar 15 '17 at 12:20
  • It is still the same question. I asked about it "also iterate over them in all supported languages" , and I mean "supported languages in the app". Are you saying there is only a single R in the generated APK, that holds the values of all others and there are no other R files in the APK ? – android developer Mar 15 '17 at 12:25
  • There's only one `R` class at runtime. Imagine you create `foo` string resource in `values-en` and `bar` in `values-de` - in your code you can **always** reference `R.string.bar` and it will build just fine as code is "language agnostic". There's no difference at build time where this comes from German or English or default assets. Also this lets you have `bar` in both language and then resolve **at runtime** what value should be returned. Same for layouts and all the `res/` stuff. It will be all referenced in singe `R` class. – Marcin Orlowski Mar 15 '17 at 12:29
  • You mean there won't be any other R class during runtime? But how come it's possible for classes of other modules to reach their R class, if this class won't exist anymore? Does the builder change the package name of this reference, to be of the global, merged one? I've just tried in debug mode to reach one of the R classes, and I succeeded. I think you are wrong to say only one exists at runtime. – android developer Mar 16 '17 at 07:45
  • About the language part, I'm not sure I understand what you mean. You mean that it might get translations of other modules, which I might not want to know about? – android developer Mar 16 '17 at 07:46
1

Here is an example combining my answer here with the code to iterate over the locales:

void foo() {
  getStringsForResource(R.strings.class, context);
  getStringsForResource(com.example.package1.R.strings.class, context);
  getStringsForResource(com.example.package2.R.strings.class, context);
  getStringsForResource(com.example.package3.R.strings.class, context);
}


void getStringsForResource(Class<?> R_strings, Context initialContext) {
  for (String locale : initialContext.getAssets().getLocales()) {
    Configuration config = new Configuration();
    config.setToDefaults();
    config.setLocale(Locale.forLanguageTag(locale));
    Context localeSpecificContext = initialContext.createConfigurationContext(config);

    getStringsForContext(R_strings, localeSpecificContext);
  }
}

void getStringsForContext(Class<?> R_strings, Context context) {
  for (Field field : R_strings.getDeclaredFields())
  {
    if (Modifier.isStatic(field.getModifiers()) && !Modifier.isPrivate(field.getModifiers()) && field.getType().equals(int.class))
    {
      try
      {
        int id = field.getInt(null);
        String resourceName = field.getName();
        String s = context.getString(id);
        // Do something with the string here
      } catch (IllegalArgumentException e)
      {
        // ignore
      } catch (IllegalAccessException e)
      {
        // ignore
      }
    }
  }

}

This code gets the list of locales based on the locales present in the resources (if you are below API 21 though, it will become more trouble than it's worth parsing this, see https://developer.android.com/reference/android/content/res/AssetManager.html#getLocales(), and you may be better off with Locale.getAvailableLocales() as in a comment on the question; be careful with what you expect out of this, though, as if a language does not exist in your resources you will get whatever Android determines is the "closest match", which will probably be en-US for a lot of them), and then uses reflection to get the string IDs in order to get all the strings. The list of resource classes is done manually; it is possible to do this part automatically as well using classpath scanning, but doing so in Android is fragile; it is better if you can just hard-code the list of resource classes to scan.

Community
  • 1
  • 1
Logan Pickup
  • 2,294
  • 2
  • 22
  • 29
  • I don't understand the part of pre-API 21. What is needed exactly? – android developer Mar 19 '17 at 09:32
  • @androiddeveloper API 21 added the `Locale.forLanguageTag()` method, and modified the output of `AssetManager.getLocales()` to return strings in the format `Locale.forLanguageTag()` expects. In the absence of this, you will have to check the format returned from `AssetManager.getLocales()` (see the documentation I linked to), and parse the language code and country code from it, so that you can pass these to the `new Locale(String language, String country)` constructor. – Logan Pickup Mar 19 '17 at 09:59
  • I see. Can you please show it in the answer as code? – android developer Mar 19 '17 at 11:24