Localizing string resources added via build.gradle using "resValue"

10,809

Solution 1

My other answer about the generated resources may be an overkill for you use case though. Base what I currently know about your project I think this one is a better fit: (not that you can still combine this with generated resources)

src/flavor1/res/values/strings.xml

<string name="app_name_base">InTouch Messenger"</string>
<string name="app_name_gpe">InTouch Messenger: GPE Edition"</string>

src/flavor1/res/values-hu/strings.xml

<string name="app_name_base">InTouch Üzenetküldő"</string>
<string name="app_name_gpe">InTouch Üzenetküldő: GPE Változat"</string>

src/flavor2/res/values/strings.xml

<string name="app_name_base">Whatever Messenger"</string>
<string name="app_name_gpe">Whatever Messenger: GPE Edition"</string>

src/flavor2/res/values-hu/strings.xml`

<string name="app_name_base">Whatever Üzenetküldő"</string>
<string name="app_name_gpe">Whatever Üzenetküldő: GPE Változat"</string>

build.gradle

android {
    sourceSets {
        [flavor1, flavor3].each {
            it.res.srcDirs = ['src/flavor1/res']
        }
        [flavor2, flavor4].each {
            it.res.srcDirs = ['src/flavor2/res']
        }
    }
    productFlavors { // notice the different numbers than sourceSets
        [flavor1, flavor2].each {
            it.resValue "string", "app_name", "@string/app_name_base"
        }
        [flavor3, flavor4].each {
            it.resValue "string", "app_name", "@string/app_name_gpe"
        }
    }
}

This means that flavor1/2 will have an extra unused app_name_gpe string resource, but that'll be taken care of by aapt:

android {
    buildTypes {
        release {
            shrinkResources true // http://tools.android.com/tech-docs/new-build-system/resource-shrinking
        }

Solution 2

If you do not have to operate on those strings, the best option is moving to strings.xml, but that would make you share all res folder between flavors. If you generate these strings based on some property on build.gradle, then I think you're out of luck, unfortunately.

EDIT: clarifying what I mean by operate above and add some options:

By operating on those strings I mean some sort of concatenation with a build parameter, a reading from command line or environment variable during the build process (e.g., getting the commit SHA1 so that it's easier to trace bugs later). If no operation is necessary, strings.xml may be an option. But when you overwrite a res folder for flavor, all of it is overwritten and that could pose a problem if several flavors share the same res except for a limited number of strings.

If each APK has its own locale, then it's just a resValue or buildConfigField in a flavor. You can define variables to for easier reuse of values. Something like

def myVar = "var"

...

flavor1 {
    resValue "string", "my_res_string", "${myVar}"
}

flavor2 {
    resValue "string", "my_res_string", "${myVar}"
}

But if several locales are needed in the same APK and it will be chosen at runtime by Android, then the string must be in the correct values-<locale> folder.

Solution 3

You're operating on different levels here, BuildConfig is code, and as such not localized, that's why we have Lint warnings for hard-coded strings. Localization in Android is done via <string resources, there's no way around that if you want the system to choose the language at runtime depending on user settings. There are many ways to have resources though: values folder, resValue in build.gradle, and generated resources.

You should look into the buildSrc project in Gradle, for example I use it to generate SQL Inserts from src/main/values/stuff.xml. Here's some code to start with.

buildSrc/build.gradle

// To enable developing buildSrc in IDEA import buildSrc/build.gradle as a separate project
// Create a settings.gradle in buildSrc as well to prevent importing as subproject
apply plugin: 'groovy'
repositories { jcenter() }
dependencies {
    compile localGroovy()
    compile gradleApi()
    testCompile 'junit:junit:4.12'
}

buildSrc/src/main/groovy/Plugin.groovy

import org.gradle.api.*
/**
 * Use it as
 * <code>
 *     apply plugin: MyPlugin
 *     myEntities {
 *         categories {
 *             input = file(path to Android res xml with Strings)
 *             output = file(path to asset SQL file)
 *             conversion = "structure|SQL"
 *         }
 *     }
 * </code>
 */
class MyPlugin implements Plugin<Project> {
    void apply(Project project) {
        def entities = project.container(MyEntity)
        // this gives the name for the block in build.gradle
        project.extensions.myEntities = entities

        def allTasks = project.task('generateYourStuff')
        def allTasksClean = project.task('cleanGenerateYourStuff')
        project.afterEvaluate {
            entities.all { entity ->
                //println "Creating task for ${entity.name} (${entity.input} --${entity.conversion}--> ${entity.output})"
                def task = project.task(type: GenerateTask, "generateYourStuff${entity.name.capitalize()}") {
                    input = entity.input
                    output = entity.output
                    conversion = entity.conversion
                }
                allTasks.dependsOn task
                // clean task is automagically generated for every task that has output
                allTasksClean.dependsOn "clean${task.name.capitalize()}"
            }
        }
    }
}
class MyEntity {
    def input
    def output
    String conversion

    final String name
    MyEntity(String name) {
        this.name = name
    }
}

buildSrc/src/main/groovy/GenerateTask.groovy

import net.twisterrob.inventory.database.*
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.*
class GenerateTask extends DefaultTask {
    @InputFile File input
    @OutputFile File output
    @Optional @Input String conversion
    @TaskAction void generate() {
        input.withReader { reader ->
            // you may need to treat output as a folder
            output.parentFile.mkdirs()
            output.withWriter { writer ->
                // custom transformation here read from reader, write to writer
            }
        }
    }
}

This is just the skeleton you can go wild and do anything from here: e.g. retrieve a CSV through the network and spread the contents into generated variant*/res/values-*/gen.xml files.

You can run it manually when you need to or run it at the right point in the build lifecycle (in build.gradle:

android.applicationVariants.all { com.android.build.gradle.api.ApplicationVariant variant ->
    variant.mergeAssets.dependsOn tasks.generateYourStuff
}
Share:
10,809
Viral Patel
Author by

Viral Patel

Updated on June 06, 2022

Comments

  • Viral Patel
    Viral Patel almost 2 years

    This is in continuation to an answer which helped me on this post

    We can add the string resource as follows from build.gradle:

    productFlavors {
        main{
            resValue "string", "app_name", "InTouch Messenger"
        }
    
        googlePlay{
            resValue "string", "app_name", "InTouch Messenger: GPE Edition"
        }
    }
    

    It works like a charm and serves the purpose of having different app names per flavor. (with the original app_name string resource deleted from strings.xml file.

    But, how do we add localized strings for this string resource added from build.gradle ?

    Is there an additional parameter we can pass specifying the locale? OR Possible to do it using a gradle task?

    Note: I cannot do this using strings.xml (not feasible because of several ways in which my project is structured)