Localizing string resources added via build.gradle using "resValue"
Asked Answered
C

3

24

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)

Claribelclarice answered 19/3, 2016 at 18:27 Comment(7)
I think that if you need to localize it, then probably shouldn't be on the gradle. The main use there is to have, for example, two different API keys, one for Prod, one for Dev.Erethism
in my case the product flavors are not for environments but for different variation of the app. all of which go to production. My environments are controlled via build type. Cant go with separate xml for each flavor because i have a lot of them and ever increasing. Hence chose to do it via gradle.Claribelclarice
Sorry for giving strings.xml solutions, but I don't think you can do it otherwise. If you have separation issues, you can add different res.srcDirs based what you're localizing, but the folder you add can contain multiple values-<lang>/strings.xml files.Upstream
By the way I think this will only change your launcher icon's label, maybe the title in the Settings app, but the application name in Play Store is based on what you enter in the Developer Console, and it's localized there.Upstream
yes that is what i wantClaribelclarice
Why can you not just use src/main/res/values/strings.xml and src/googlePlay/res/values/strings.xml to add your localized, flavored strings how it would be intended? Even if you could just add it to the build script, the result would be the same as if you were just using those folders.Coagulase
@david those two flavors above are just what I copied from the other question. I have too many flavors to create separate folders.Claribelclarice
U
13

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
        }
Upstream answered 29/3, 2016 at 10:37 Comment(2)
@AndroidMechanic did you have any luck with either?Upstream
No did not work how I wanted. Have created separate xmls for now. But there should be some way around this.Claribelclarice
J
5

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.

Joettajoette answered 29/3, 2016 at 4:36 Comment(4)
Any chance we can use a task to replace the string res from build config field prior to build?Claribelclarice
You mean so the string is fixed for a given APK? For instance, one APK only for en_rUS locale, other only for pt_rBR, other for ja? This way, yes, it's possible. If so, I'll update my answer. What is not possible is having multiple locales in same APK, such as what we do with values-en, values-pt, etc. For this, you'd need the res/ folderJoettajoette
oh no sorry my bad. did not think it through. sorry! :) it is not fixed. need localizations for each locale.Claribelclarice
Turns out there is currently no available way to do this without using separate strings.xml. Hence awarding the bounty to this answer. But hope there will be something made available in the framework going forward. If someone gets to know of it do post an answer here. Thanks Douglas!Claribelclarice
U
2

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
}
Upstream answered 29/3, 2016 at 10:14 Comment(1)
thanks! will check this one as well as soon as i'm homeClaribelclarice

© 2022 - 2024 — McMap. All rights reserved.