1

I have an Android app, and for this app I automatically download translations from crowdin.com - this works well.

Some languages make use of apostrophes and it is known that if you have apostrophes in your strings.xml, then you should escape them.

What I am looking to do is fail the build if there are unescaped apostrophes in my various strings.xml.

Specifically I want to search for all strings.xml in src/main/res/ and if any files contain an ' not preceded by \, then throw an error. This would be ideal for me as I can know about it, and correct the translations at the source with the translators.

Alternatively I am looking to replace the unescaped apostrophes, but in an automated manner as part of the build itself.

Mendhak
  • 8,194
  • 5
  • 47
  • 64
  • you can fail build with something like `throw new GradleException('error occurred')` in task, so after downloading file, you could read it in gradle task and if aphostrophe is found in some line then throw that exception. If you want more detailed answer, than please include related parts of build file and additional related files, so we could understand your case. – itwasntme Feb 08 '20 at 11:09
  • Edited post and added filename and paths. > "you could read it in gradle task and if aphostrophe is found" Yes that's what I'm asking, how to search for a string/regex in a directory in a Gradle task – Mendhak Feb 08 '20 at 11:31

1 Answers1

2

Here's a simple example of a gradle task executed before "preBuild" which checks all strings.xml it finds for string values with unescaped apostrophes. If errors found, fails build. Change the dependency as needed for your download task.

(Note the script fails on first error encountered - so if there is both an error in the en version and es version then only one file is reported as error.)

(Note2 since an XML parser is used it is only processing string-tagged values - and consequently also ignores any violations in xml comments - this is evident in the first test case of no errors.)

(In Module: app (build.gradle))

task checkUnescapedApostrophes {
    doFirst {

        println("checkUnescapedApostrophes")

        fileTree("src").matching {
            include "**/strings.xml"
        }.each {
            def stringsFile = it
            def parser = (new XmlParser()).parse(stringsFile)
            println("Processing file: "+stringsFile)
            parser.'string'.each { m ->
                def s = m.text()
                def ss = "[^\\\\]\'"
                println "[" + m.@name + "]: " + s
                if (s =~ ss) {
                    throw new GradleException(
                        "Found a string value in " + stringsFile + 
                        " have unescaped apostrophe: "+s)
                }
            }
        }
        println("strings.xml OK")
    }
}

preBuild.dependsOn(checkUnescapedApostrophes)

In no-errors case of strings.xml (default locale + es):

<resources>
<!--    <string name="test">some value with '</string> -->
    <string name="test2">some with escaped \'</string>
</resources>

<resources>
    <!--    <string name="test">algún valor con apóstrofe sin escape '</string> -->
    <string name="test2">algún valor con el apóstrofe escapado \'</string>
</resources>

Build Output:

> Configure project :app
checkUnescapedApostrophes
Processing file: ...\app\src\main\res\values\strings.xml
[test2]: some with escaped \'
Processing file: ....\app\src\main\res\values-es\strings.xml
[test2]: algún error con el apóstrofe escapado \'    
strings.xml OK

> Task :app:checkUnescapedApostrophes UP-TO-DATE
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:compileDebugAidl NO-SOURCE
> Task :app:compileDebugRenderscript NO-SOURCE
> Task :app:checkDebugManifest UP-TO-DATE
> Task :app:generateDebugBuildConfig UP-TO-DATE
> Task :app:prepareLintJar UP-TO-DATE
> Task :app:prepareLintJarForPublish UP-TO-DATE
> Task :app:generateDebugSources UP-TO-DATE

BUILD SUCCESSFUL in 1s
4 actionable tasks: 4 up-to-date

And in error case of strings.xml (in default local and es)

<resources>
    <string name="test">some value with '</string>
    <string name="test2">some with escaped \'</string>
</resources>

<resources>
    <string name="test">algún valor con apóstrofe sin escape '</string>
    <string name="test2">algún valor con el apóstrofe escapado \'</string>
</resources>

(Note that in the above cases the unescaped apostraphe is red-flagged in studio with an error - as expected.)

Build Output:

> Configure project :app
checkUnescapedApostrophes
Processing file: ...\app\src\main\res\values\strings.xml
[test]: some value with '

FAILURE: Build failed with an exception.

* Where:
Build file '...\app\build.gradle' line: 43

* What went wrong:
A problem occurred evaluating project ':app'.
> Found a string value in ...\app\src\main\res\values\strings.xml have unescaped apostrophe: some value with '

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 1s

General gradle note: when testing for yourself and changing build.gradle - be sure to 'Rebuild Project' and not just 'Build Project' as any gradle changes don't seem to be picked up in the case when the build previously failed.


Also as an alternative which you mentioned this task will escape unescaped apostrophes by replacing in all strings.xml found. This is an in-place update - there is a more robust approach here: https://stackoverflow.com/a/48356111/2711811.

task replaceUnescapedApostrophes {
    doFirst {
        ant.replaceregexp(match: "([^\\\\])'", replace: "\\1\\\\\\\\'",  byline: "true") {
            fileset(dir: 'src/', excludes: '*', includes: '**/*strings.xml')
        }
    }
}

//preBuild.dependsOn(checkUnescapedApostrophes)
preBuild.dependsOn(replaceUnescapedApostrophes)
  • Thank you, you actually did both! It's appreciated. Also thanks for the 'rebuild project' vs 'build project'. – Mendhak Feb 10 '20 at 20:30
  • What is the \\1\\\\\\\\ ? Is \\1 the result of the match capture, followed by a double escaped `\'`? – Mendhak Feb 10 '20 at 20:36
  • Correct, `\\1` is the capture of preceding character which wasn't an escape character and the remaining backslashes is just madness but results in what you indicated. Now that I think about it perhaps the capture-replace part is not necessary - i'll test later and update if necessary. (actually its needed since the replace would remove preceding character) –  Feb 10 '20 at 21:53
  • For those using kts, you can use that: ant.withGroovyBuilder { "replaceregexp"("match" to "([^\\\\])'", "replace" to "\\1\\\\\\\\'", "byline" to "true") { "fileset"("dir" to ".") { "include"("name" to "**/*strings.xml") } } } – user1998494 Feb 10 '23 at 17:14