LeOSium-WF/app/build.gradle

715 lines
28 KiB
Groovy

import com.android.build.OutputFile
import groovy.json.JsonOutput
import net.waterfox.android.gradle.tasks.ApkSizeTask
plugins {
alias libs.plugins.android.application
alias libs.plugins.kotlin.android
alias libs.plugins.androidx.safeargs
alias libs.plugins.kotlin.parcelize
alias libs.plugins.compose.compiler
alias libs.plugins.play.publisher
id "com.jetbrains.python.envs" version "0.0.26"
id 'jacoco'
id 'com.google.android.gms.oss-licenses-plugin'
id("app.accrescent.tools.bundletool") version "0.2.4"
}
bundletool {
// Only configure signing if the KEYSTORE environment variable is set
if (System.getenv("KEYSTORE") != null) {
signingConfig {
storeFile = file(System.getenv("KEYSTORE"))
storePassword = System.getenv("KEYSTORE_PWD")
keyAlias = System.getenv("KEY_ALIAS")
keyPassword = System.getenv("KEY_ALIAS_PWD")
}
}
}
import org.gradle.internal.logging.text.StyledTextOutput.Style
import org.gradle.internal.logging.text.StyledTextOutputFactory
import static org.gradle.api.tasks.testing.TestResult.ResultType
apply from: 'benchmark.gradle'
play {
// The plugin will read the service account credentials from the
// ANDROID_PUBLISHER_CREDENTIALS environment variable if it's set
enabled.set(gradle.hasProperty("enablePlayPublisher") || System.getenv("ANDROID_PUBLISHER_CREDENTIALS") != null)
// Only publish release builds
defaultToAppBundles.set(true)
track.set("internal") // Start with internal track for safety
}
android {
project.maybeConfigForJetpackBenchmark(it)
if (project.hasProperty("testBuildType")) {
// Allowing to configure the test build type via command line flag (./gradlew -PtestBuildType=release ..)
// in order to run UI tests against other build variants than debug in automation.
testBuildType project.property("testBuildType")
}
defaultConfig {
applicationId "com.leos.leosium"
minSdkVersion Config.minSdkVersion
compileSdk Config.compileSdkVersion
targetSdkVersion Config.targetSdkVersion
versionCode 1
versionName Config.generateDebugVersionName(project)
vectorDrawables.useSupportLibrary = true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
resValue "bool", "IS_DEBUG", "false"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "false"
buildConfigField "String", "GIT_HASH", "\"\"" // see override in release builds for why it's blank.
// This should be the "public" base URL of AMO.
buildConfigField "String", "AMO_BASE_URL", "\"https://addons.mozilla.org\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"LeOSium-Android\""
buildConfigField "String", "AMO_COLLECTION_USER", "\"17224042\""
// This should be the base URL used to call the AMO API.
buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\""
def deepLinkSchemeValue = "waterfox-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue
]
buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", getSupportedLocales()
}
def releaseTemplate = {
// We allow disabling optimization by passing `-PdisableOptimization` to gradle. This is used
// in automation for UI testing non-debug builds.
shrinkResources !project.hasProperty("disableOptimization")
minifyEnabled !project.hasProperty("disableOptimization")
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
matchingFallbacks = ['release'] // Use on the "release" build type in dependencies (AARs)
// Changing the build config can cause files that depend on BuildConfig.java to recompile
// so we only set the git hash in release builds to avoid possible recompilation in debug builds.
// TODO: [Waterfox] This times out for some reason. Find a solution.
// buildConfigField "String", "GIT_HASH", "\"${Config.getGitHash()}\""
if (gradle.hasProperty("localProperties.autosignReleaseWithDebugKey")) {
signingConfig signingConfigs.debug
}
if (gradle.hasProperty("localProperties.debuggable")) {
debuggable true
}
}
buildTypes {
debug {
shrinkResources false
minifyEnabled false
applicationIdSuffix ".debug"
resValue "bool", "IS_DEBUG", "true"
pseudoLocalesEnabled true
testCoverageEnabled true
}
release releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
applicationIdSuffix ""
def deepLinkSchemeValue = "waterfox"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue
]
}
}
buildFeatures {
viewBinding true
buildConfig true
}
androidResources {
// All JavaScript code used internally by GeckoView is packaged in a
// file called omni.ja. If this file is compressed in the APK,
// GeckoView must uncompress it before it can do anything else which
// causes a significant delay on startup.
noCompress 'ja'
// manifest.template.json is converted to manifest.json at build time.
// No need to package the template in the APK.
ignoreAssetsPattern "manifest.template.json"
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
unitTests.includeAndroidResources = true
animationsDisabled = true
}
flavorDimensions.add("product")
productFlavors {
waterfox {
dimension "product"
}
}
sourceSets {
androidTest {
resources.srcDirs += ['src/androidTest/resources']
}
}
splits {
abi {
enable true
reset()
include "armeabi-v7a", "arm64-v8a"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
coreLibraryDesugaringEnabled true
}
lint {
lintConfig file("lint.xml")
baseline file("lint-baseline.xml")
}
packagingOptions {
resources {
excludes += ['META-INF/atomicfu.kotlin_module', 'META-INF/AL2.0', 'META-INF/LGPL2.1']
}
jniLibs {
useLegacyPackaging true
}
}
testOptions {
unitTests.returnDefaultValues = true
unitTests.all {
// We keep running into memory issues when running our tests. With this config we
// reserve more memory and also create a new process after every 80 test classes. This
// is a band-aid solution and eventually we should try to find and fix the leaks
// instead. :)
forkEvery = 80
maxHeapSize = "3072m"
minHeapSize = "1024m"
}
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.androidx_compose_compiler
}
namespace 'net.waterfox.android'
}
android.applicationVariants.all { variant ->
// -------------------------------------------------------------------------------------------------
// Generate version codes for builds
// -------------------------------------------------------------------------------------------------
def isDebug = variant.buildType.resValues['IS_DEBUG']?.value ?: false
def useReleaseVersioning = variant.buildType.buildConfigFields['USE_RELEASE_VERSIONING']?.value ?: false
println("----------------------------------------------")
println("Variant name: " + variant.name)
println("Application ID: " + [variant.applicationId, variant.buildType.applicationIdSuffix].findAll().join())
println("Build type: " + variant.buildType.name)
println("Flavor: " + variant.flavorName)
if (useReleaseVersioning) {
// The Google Play Store does not allow multiple APKs for the same app that all have the
// same version code. Therefore we need to have different version codes for our ARM and x86
// builds.
def versionName = Config.releaseVersionName(project)
println("versionName override: $versionName")
variant.outputs.each { output ->
def abi = output.getFilter(OutputFile.ABI)
// We use the same version code generator, that we inherited from Fennec, across all channels - even on
// channels that never shipped a Fennec build.
def versionCodeOverride = Config.generateFennecVersionCode()
println("versionCode for $abi = $versionCodeOverride")
output.versionNameOverride = versionName
output.versionCodeOverride = versionCodeOverride
}
} else if (gradle.hasProperty("localProperties.branchBuild.waterfox.version")) {
def versionName = gradle.getProperty("localProperties.branchBuild.waterfox.version")
println("versionName override: $versionName")
variant.outputs.each { output ->
output.versionNameOverride = versionName
}
}
// -------------------------------------------------------------------------------------------------
// BuildConfig: Set variables for Sentry and Crash Reporting
// -------------------------------------------------------------------------------------------------
buildConfigField 'String', 'SENTRY_TOKEN', 'null'
if (!isDebug) {
buildConfigField 'boolean', 'CRASH_REPORTING', 'false'
// Reading sentry token from local file (if it exists). In a release task on Fastlane it will be available.
try {
def token = new File("${rootDir}/.sentry_token").text.trim()
buildConfigField 'String', 'SENTRY_TOKEN', '"' + token + '"'
} catch (FileNotFoundException ignored) {}
} else {
buildConfigField 'boolean', 'CRASH_REPORTING', 'false'
}
def buildDate = Config.generateBuildDate()
// Setting buildDate with every build changes the generated BuildConfig, which slows down the
// build. Only do this for non-debug builds, to speed-up builds produced during local development.
if (isDebug) {
buildConfigField 'String', 'BUILD_DATE', '"debug build"'
} else {
buildConfigField 'String', 'BUILD_DATE', '"' + buildDate + '"'
}
// -------------------------------------------------------------------------------------------------
// MLS: Read token from local file if it exists
// -------------------------------------------------------------------------------------------------
print("MLS token: ")
try {
def token = new File("${rootDir}/.mls_token").text.trim()
buildConfigField 'String', 'MLS_TOKEN', '"' + token + '"'
println "(Added from .mls_token file)"
} catch (FileNotFoundException ignored) {
buildConfigField 'String', 'MLS_TOKEN', '""'
println("X_X")
}
// -------------------------------------------------------------------------------------------------
// BuildConfig: Set flag for official builds; similar to MOZILLA_OFFICIAL in mozilla-central.
// -------------------------------------------------------------------------------------------------
if (project.hasProperty("official") || gradle.hasProperty("localProperties.official")) {
buildConfigField 'Boolean', 'MOZILLA_OFFICIAL', 'true'
} else {
buildConfigField 'Boolean', 'MOZILLA_OFFICIAL', 'false'
}
// -------------------------------------------------------------------------------------------------
// BuildConfig: Set remote wallpaper URL using local file if it exists
// -------------------------------------------------------------------------------------------------
print("Wallpaper URL: ")
try {
def token = new File("${rootDir}/.wallpaper_url").text.trim()
buildConfigField 'String', 'WALLPAPER_URL', '"' + token + '"'
println "(Added from .wallpaper_url file)"
} catch (FileNotFoundException ignored) {
buildConfigField 'String', 'WALLPAPER_URL', '""'
println("--")
}
}
// [Waterfox] try to exclude all telemetry dependencies
configurations.all {
// Telemetry is a transitive dependency of several necessary dependencies,
// thus we have to exclude it globally.
exclude group: 'org.mozilla.telemetry', module: 'glean-native'
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
}
}
dependencies {
implementation libs.mozilla.browser.engine.gecko
implementation libs.kotlin.stdlib
implementation libs.kotlin.coroutines
implementation libs.kotlin.coroutines.android
testImplementation libs.kotlin.coroutines.test
implementation libs.androidx.appcompat
implementation libs.androidx.activity.compose
implementation libs.androidx.constraintlayout
implementation libs.androidx.coordinatorlayout
implementation libs.google.accompanist.drawablepainter
implementation libs.google.accompanist.insets
implementation libs.google.accompanist.swiperefresh
implementation libs.coil
// implementation libs.sentry
implementation libs.mozilla.compose.awesomebar
implementation libs.mozilla.concept.awesomebar
implementation libs.mozilla.concept.base
implementation libs.mozilla.concept.engine
implementation libs.mozilla.concept.menu
implementation libs.mozilla.concept.push
implementation libs.mozilla.concept.storage
implementation libs.mozilla.concept.sync
implementation libs.mozilla.concept.toolbar
implementation libs.mozilla.concept.tabstray
implementation libs.mozilla.browser.domains
implementation libs.mozilla.browser.icons
implementation libs.mozilla.browser.menu
implementation libs.mozilla.browser.menu2
implementation libs.mozilla.browser.session.storage
implementation libs.mozilla.browser.state
implementation libs.mozilla.browser.storage.sync
implementation libs.mozilla.browser.tabstray
implementation libs.mozilla.browser.thumbnails
implementation libs.mozilla.browser.toolbar
implementation libs.mozilla.feature.addons
implementation libs.mozilla.feature.accounts
implementation libs.mozilla.feature.app.links
implementation libs.mozilla.feature.autofill
implementation libs.mozilla.feature.awesomebar
implementation libs.mozilla.feature.contextmenu
implementation libs.mozilla.feature.customtabs
implementation libs.mozilla.feature.downloads
implementation libs.mozilla.feature.intent
implementation libs.mozilla.feature.media
implementation libs.mozilla.feature.prompts
implementation libs.mozilla.feature.push
implementation libs.mozilla.feature.privatemode
implementation libs.mozilla.feature.pwa
implementation libs.mozilla.feature.qr
implementation libs.mozilla.feature.search
implementation libs.mozilla.feature.session
implementation libs.mozilla.feature.syncedtabs
implementation libs.mozilla.feature.toolbar
implementation libs.mozilla.feature.tabs
implementation libs.mozilla.feature.findinpage
implementation libs.mozilla.feature.logins
implementation libs.mozilla.feature.site.permissions
implementation libs.mozilla.feature.readerview
implementation libs.mozilla.feature.tab.collections
implementation libs.mozilla.feature.recentlyclosed
implementation libs.mozilla.feature.top.sites
implementation libs.mozilla.feature.share
implementation libs.mozilla.feature.accounts.push
implementation libs.mozilla.feature.webauthn
implementation libs.mozilla.feature.webcompat
implementation libs.mozilla.feature.webnotifications
implementation libs.mozilla.feature.webcompat.reporter
implementation libs.mozilla.service.mars
implementation libs.mozilla.service.digitalassetlinks
implementation libs.mozilla.service.sync.autofill
implementation libs.mozilla.service.sync.logins
implementation libs.mozilla.service.firefox.accounts
implementation libs.mozilla.service.location
implementation libs.mozilla.support.extensions
implementation libs.mozilla.support.base
implementation libs.mozilla.support.rusterrors
implementation libs.mozilla.support.images
implementation libs.mozilla.support.ktx
implementation libs.mozilla.support.rustlog
implementation libs.mozilla.support.utils
implementation libs.mozilla.support.locale
implementation libs.mozilla.ui.colors
implementation libs.mozilla.ui.icons
implementation libs.mozilla.lib.publicsuffixlist
implementation libs.mozilla.ui.widgets
implementation libs.mozilla.ui.tabcounter
implementation libs.mozilla.lib.crash
// implementation libs.lib.crash.sentry
implementation libs.mozilla.lib.push.firebase
implementation libs.mozilla.lib.state
implementation libs.mozilla.lib.dataprotect
debugImplementation libs.leakcanary
implementation libs.androidx.compose.ui
implementation libs.androidx.compose.ui.tooling
implementation libs.androidx.compose.foundation
implementation libs.androidx.compose.material
implementation libs.androidx.compose.paging
implementation libs.androidx.legacy
implementation libs.androidx.biometric
implementation libs.androidx.paging
implementation libs.androidx.preference
implementation libs.androidx.fragment
implementation libs.androidx.navigation.fragment
implementation libs.androidx.navigation.ui
implementation libs.androidx.recyclerview
implementation libs.androidx.lifecycle.common
implementation libs.androidx.lifecycle.livedata
implementation libs.androidx.lifecycle.process
implementation libs.androidx.lifecycle.runtime
implementation libs.androidx.lifecycle.viewmodel
implementation libs.androidx.core
implementation libs.androidx.core.ktx
implementation libs.androidx.transition
implementation libs.androidx.work.ktx
implementation libs.androidx.datastore
implementation libs.google.material
androidTestImplementation libs.uiautomator
androidTestImplementation "tools.fastlane:screengrab:2.0.0"
// This Falcon version is added to maven central now required for Screengrab
implementation 'com.jraska:falcon:2.2.0'
androidTestImplementation libs.androidx.compose.ui.test.manifest
androidTestImplementation libs.espresso.core, {
exclude group: 'com.android.support', module: 'support-annotations'
}
androidTestImplementation libs.espresso.contrib, {
exclude module: 'appcompat-v7'
exclude module: 'support-v4'
exclude module: 'support-annotations'
exclude module: 'recyclerview-v7'
exclude module: 'design'
exclude module: 'espresso-core'
exclude module: 'protobuf-lite'
}
androidTestImplementation libs.androidx.test.core
androidTestImplementation libs.espresso.idling.resources
androidTestImplementation libs.espresso.intents
androidTestImplementation libs.tools.test.runner
androidTestImplementation libs.tools.test.rules
androidTestUtil libs.orchestrator
androidTestImplementation libs.espresso.core, {
exclude group: 'com.android.support', module: 'support-annotations'
}
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.test.extensions
androidTestImplementation libs.androidx.work.testing
androidTestImplementation libs.androidx.benchmark.junit4
androidTestImplementation libs.mockwebserver
androidTestImplementation libs.androidx.compose.ui.test
testImplementation libs.mozilla.support.test
testImplementation libs.mozilla.support.test.libstate
testImplementation libs.androidx.junit
testImplementation libs.androidx.test.extensions
testImplementation libs.androidx.work.testing
testImplementation (libs.robolectric) {
exclude group: 'org.apache.maven'
}
testImplementation 'org.apache.maven:maven-ant-tasks:2.1.3'
implementation libs.mozilla.support.rusthttp
testImplementation libs.mockk
lintChecks project(":mozilla-lint-rules")
coreLibraryDesugaring libs.desugar
}
if (project.hasProperty("coverage")) {
tasks.withType(Test).configureEach {
jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*']
}
jacoco {
toolVersion = "0.8.7"
}
android.applicationVariants.all { variant ->
tasks.register("jacoco${variant.name.capitalize()}TestReport", JacocoReport) {
dependsOn "test${variant.name.capitalize()}UnitTest"
reports {
xml.enabled = true
html.enabled = true
}
def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*',
'**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*']
def kotlinDebugTree = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/${variant.name}", excludes: fileFilter)
def javaDebugTree = fileTree(dir: "$project.buildDir/intermediates/classes/${variant.flavorName}/${variant.buildType.name}",
excludes: fileFilter)
def mainSrc = "$project.projectDir/src/main/java"
sourceDirectories.setFrom(files([mainSrc]))
classDirectories.setFrom(files([kotlinDebugTree, javaDebugTree]))
executionData.setFrom(fileTree(dir: project.buildDir, includes: [
"jacoco/test${variant.name.capitalize()}UnitTest.exec",
'outputs/code-coverage/connected/*coverage.ec'
]))
}
}
}
// -------------------------------------------------------------------------------------------------
// Task for printing APK information for the requested variant
// Usage: "./gradlew printVariants
// -------------------------------------------------------------------------------------------------
tasks.register('printVariants') {
doLast {
def variants = android.applicationVariants.collect { variant -> [
apks: variant.outputs.collect { output -> [
abi: output.getFilter(com.android.build.VariantOutput.FilterType.ABI),
fileName: output.outputFile.name
]},
build_type: variant.buildType.name,
name: variant.name,
]}
// AndroidTest is a special case not included above
variants.add([
apks: [[
abi: 'noarch',
fileName: 'app-debug-androidTest.apk',
]],
build_type: 'androidTest',
name: 'androidTest',
])
println 'variants: ' + JsonOutput.toJson(variants)
}
}
afterEvaluate {
// Format test output. Ported from AC #2401
tasks.withType(Test).configureEach {
systemProperty "robolectric.logging", "stdout"
systemProperty "logging.test-mode", "true"
testLogging.events = []
def out = services.get(StyledTextOutputFactory).create("tests")
beforeSuite { descriptor ->
if (descriptor.getClassName() != null) {
out.style(Style.Header).println("\nSUITE: " + descriptor.getClassName())
}
}
beforeTest { descriptor ->
out.style(Style.Description).println(" TEST: " + descriptor.getName())
}
onOutput { descriptor, event ->
logger.lifecycle(" " + event.message.trim())
}
afterTest { descriptor, result ->
switch (result.getResultType()) {
case ResultType.SUCCESS:
out.style(Style.Success).println(" SUCCESS")
break
case ResultType.FAILURE:
out.style(Style.Failure).println(" FAILURE")
logger.lifecycle("", result.getException())
break
case ResultType.SKIPPED:
out.style(Style.Info).println(" SKIPPED")
break
}
logger.lifecycle("")
}
}
}
if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopsrcdir')) {
if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopobjdir')) {
ext.topobjdir = gradle."localProperties.dependencySubstitutions.geckoviewTopobjdir"
}
ext.topsrcdir = gradle."localProperties.dependencySubstitutions.geckoviewTopsrcdir"
apply from: "${topsrcdir}/substitute-local-geckoview.gradle"
}
def acSrcDir = null
if (gradle.hasProperty('localProperties.autoPublish.android-components.dir')) {
acSrcDir = gradle.getProperty('localProperties.autoPublish.android-components.dir')
} else if (gradle.hasProperty('localProperties.branchBuild.android-components.dir')) {
acSrcDir = gradle.getProperty('localProperties.branchBuild.android-components.dir')
}
if (acSrcDir) {
if (acSrcDir.startsWith("/")) {
apply from: "${acSrcDir}/substitute-local-ac.gradle"
} else {
apply from: "../${acSrcDir}/substitute-local-ac.gradle"
}
}
def appServicesSrcDir = null
if (gradle.hasProperty('localProperties.autoPublish.application-services.dir')) {
appServicesSrcDir = gradle.getProperty('localProperties.autoPublish.application-services.dir')
} else if (gradle.hasProperty('localProperties.branchBuild.application-services.dir')) {
appServicesSrcDir = gradle.getProperty('localProperties.branchBuild.application-services.dir')
}
if (appServicesSrcDir) {
if (appServicesSrcDir.startsWith("/")) {
apply from: "${appServicesSrcDir}/build-scripts/substitute-local-appservices.gradle"
} else {
apply from: "../${appServicesSrcDir}/build-scripts/substitute-local-appservices.gradle"
}
}
// Define a reusable task for updating the versions of our built-in web extensions. We automate this
// to make sure we never forget to update the version, either in local development or for releases.
// In both cases, we want to make sure the latest version of all extensions (including their latest
// changes) are installed on first start-up.
// We're using the A-C version here as we want to uplift all built-in extensions to A-C (Once that's
// done we can also remove the task below):
// https://github.com/mozilla-mobile/android-components/issues/7249
ext.updateExtensionVersion = { task, extDir ->
configure(task) {
from extDir
include 'manifest.template.json'
rename { 'manifest.json' }
into extDir
def values = ['version': Versions.mozilla_android_components + "." + new Date().format('MMddHHmmss')]
inputs.properties(values)
expand(values)
}
}
android.applicationVariants.configureEach { variant ->
tasks.register("apkSize${variant.name.capitalize()}", ApkSizeTask) {
variantName = variant.name
apks = variant.outputs.collect { output -> output.outputFile.name }
dependsOn "package${variant.name.capitalize()}"
}
}
def getSupportedLocales() {
// This isn't running as a task, instead the array is build when the gradle file is parsed.
// https://github.com/mozilla-mobile/fenix/issues/14175
def foundLocales = new StringBuilder()
foundLocales.append("new String[]{")
fileTree("src/main/res").visit { FileVisitDetails details ->
if (details.file.path.endsWith("${File.separator}strings.xml")) {
def languageCode = details.file.parent.tokenize(File.separator).last().replaceAll('values-', '').replaceAll('-r', '-')
languageCode = (languageCode == "values") ? "en-US" : languageCode
foundLocales.append("\"").append(languageCode).append("\"").append(",")
}
}
foundLocales.append("}")
def foundLocalesString = foundLocales.toString().replaceAll(',}', '}')
return foundLocalesString
}