Skip to content

Commit 6f1bab5

Browse files
authored
chore: add crowdin entries automagically [WPB-8645] (#4518)
1 parent 559b7af commit 6f1bab5

File tree

7 files changed

+150
-22
lines changed

7 files changed

+150
-22
lines changed

build-logic/plugins/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2020

2121
plugins {
2222
`kotlin-dsl`
23+
alias(libs.plugins.kotlin.serialization)
2324
}
2425

2526
// Configure the build-logic plugins to target JDK 17
@@ -41,6 +42,7 @@ dependencies {
4142
testImplementation(libs.junit4)
4243
testImplementation(kotlin("test"))
4344
implementation(libs.ksp.symbol.processing.plugin)
45+
implementation(libs.ktx.serialization)
4446
}
4547

4648
gradlePlugin {

build-logic/plugins/src/main/kotlin/AndroidLibraryConventionPlugin.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.android.build.gradle.LibraryExtension
1919
import com.wire.android.gradle.configureAndroidKotlinTests
2020
import com.wire.android.gradle.configureCompose
2121
import com.wire.android.gradle.configureKotlinAndroid
22+
import com.wire.android.gradle.crowdin.AddEntryToCrowdinTask
2223
import org.gradle.api.Plugin
2324
import org.gradle.api.Project
2425
import org.gradle.kotlin.dsl.configure
@@ -50,6 +51,17 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
5051
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
5152
}
5253
}
54+
55+
val crowdinTask = tasks.register("addEntryToCrowdinFile", AddEntryToCrowdinTask::class.java) {
56+
resDirPath = sourceSets.getByName("main").res.srcDirs.first().path
57+
}
58+
59+
afterEvaluate {
60+
val resTasks = tasks.filter { it.name.startsWith("package") && it.name.endsWith("Resources") }
61+
resTasks.forEach {
62+
it.dependsOn(crowdinTask)
63+
}
64+
}
5365
}
5466
}
5567
}

build-logic/plugins/src/main/kotlin/KoverConventionPlugin.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,8 @@ class KoverConventionPlugin : Plugin<Project> {
2828
extensions.configure<KoverReportExtension> {
2929
defaults {
3030
if (project.name == "app") {
31-
println(">> Configuring Kover devDebug variant for: ${project.name}")
3231
mergeWith("devDebug")
3332
} else {
34-
println(">> Configuring Kover debug variant for: ${project.name}")
3533
mergeWith("debug")
3634
}
3735

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2026 Wire Swiss GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see http://www.gnu.org/licenses/.
17+
*/
18+
package com.wire.android.gradle.crowdin
19+
20+
import kotlinx.serialization.json.Json
21+
import org.gradle.api.DefaultTask
22+
import org.gradle.api.Project
23+
import org.gradle.api.tasks.Input
24+
import org.gradle.api.tasks.OutputFile
25+
import org.gradle.api.tasks.TaskAction
26+
import java.io.File
27+
28+
abstract class AddEntryToCrowdinTask : DefaultTask() {
29+
30+
@get:Input
31+
abstract var resDirPath: String
32+
33+
private val rootProject: Project
34+
get() = project.rootProject
35+
36+
@OutputFile
37+
val outputFile: File = project.rootDir.resolve(CROWDIN_FILE_NAME)
38+
39+
@OptIn(ExperimentalStdlibApi::class)
40+
@TaskAction
41+
fun taskAction() {
42+
val expectedStringFileEntry = File(resDirPath)
43+
.resolve(DEFAULT_SOURCE_STRING_FILE_PATH)
44+
.takeIf { it.exists() }
45+
?.relativeTo(rootProject.rootDir)
46+
?.invariantSeparatorsPath
47+
?.let { "/$it" } ?: return
48+
val crowdinFile = outputFile
49+
val crowdinFileContent = crowdinFile.readText()
50+
val filesEntry = crowdinFileContent.substringAfter(FILES_ENTRY_DELIMITER)
51+
val restOfFile = crowdinFileContent.substring(0..<crowdinFileContent.length - filesEntry.length)
52+
val json = Json { prettyPrint = true }
53+
val entries = json.decodeFromString<List<CrowdinFileEntry>>(filesEntry).toMutableList()
54+
val isNewEntryNeeded = expectedStringFileEntry !in entries.map { it.source }
55+
if (!isNewEntryNeeded) return
56+
57+
val newEntry = CrowdinFileEntry(
58+
source = expectedStringFileEntry,
59+
translation = expectedStringFileEntry.replace(DEFAULT_SOURCE_STRING_FILE_PATH, TRANSLATION_RES_STRING_FILE_PATH)
60+
)
61+
entries.add(newEntry)
62+
println("Adding new entry to Crowdin file: ${json.encodeToString(CrowdinFileEntry.serializer(), newEntry)}")
63+
64+
val newFilesEntryContent = json.encodeToString(entries.sortedBy { it.source })
65+
crowdinFile.writeText(restOfFile + newFilesEntryContent)
66+
}
67+
68+
private companion object {
69+
const val DEFAULT_SOURCE_STRING_FILE_PATH = "values/strings.xml"
70+
const val TRANSLATION_RES_STRING_FILE_PATH = "values-%two_letters_code%/%original_file_name%"
71+
const val CROWDIN_FILE_NAME = "crowdin.yml"
72+
const val FILES_ENTRY_DELIMITER = "files: "
73+
}
74+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2026 Wire Swiss GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see http://www.gnu.org/licenses/.
17+
*/
18+
package com.wire.android.gradle.crowdin
19+
20+
import kotlinx.serialization.SerialName
21+
import kotlinx.serialization.Serializable
22+
23+
@Serializable
24+
data class CrowdinFileEntry(
25+
@SerialName("source") val source: String,
26+
@SerialName("translation") val translation: String
27+
)

crowdin.yml

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,39 @@
44
"base_url": "https://api.crowdin.com"
55
"preserve_hierarchy": true
66

7+
# Entries with the format '/**/src/main/res/values/string.xml' are added automatically
8+
# via the addEntryToCrowdinFile tasks, but they are NOT removed automatically.
79
files: [
8-
{
9-
"source": "/app/src/main/res/values/strings.xml",
10-
"translation": "/app/src/main/res/values-%two_letters_code%/%original_file_name%"
11-
},
12-
{
13-
"source": "/features/cells/src/main/res/values/strings.xml",
14-
"translation": "/features/cells/src/main/res/values-%two_letters_code%/%original_file_name%"
15-
},
16-
{
17-
"source": "/features/sketch/src/main/res/values/strings.xml",
18-
"translation": "/features/sketch/src/main/res/values-%two_letters_code%/%original_file_name%"
19-
},
20-
{
21-
"source": "/features/meetings/src/main/res/values/strings.xml",
22-
"translation": "/features/meetings/src/main/res/values-%two_letters_code%/%original_file_name%"
23-
},
24-
{
25-
"source": "/core/ui-common/src/main/res/values/strings.xml",
26-
"translation": "/core/ui-common/src/main/res/values-%two_letters_code%/%original_file_name%"
27-
}
10+
{
11+
"source": "/app/src/main/res/values/strings.xml",
12+
"translation": "/app/src/main/res/values-%two_letters_code%/%original_file_name%"
13+
},
14+
{
15+
"source": "/core/analytics-disabled/src/main/res/values/strings.xml",
16+
"translation": "/core/analytics-disabled/src/main/res/values-%two_letters_code%/%original_file_name%"
17+
},
18+
{
19+
"source": "/core/analytics-enabled/src/main/res/values/strings.xml",
20+
"translation": "/core/analytics-enabled/src/main/res/values-%two_letters_code%/%original_file_name%"
21+
},
22+
{
23+
"source": "/core/analytics/src/main/res/values/strings.xml",
24+
"translation": "/core/analytics/src/main/res/values-%two_letters_code%/%original_file_name%"
25+
},
26+
{
27+
"source": "/core/ui-common/src/main/res/values/strings.xml",
28+
"translation": "/core/ui-common/src/main/res/values-%two_letters_code%/%original_file_name%"
29+
},
30+
{
31+
"source": "/features/cells/src/main/res/values/strings.xml",
32+
"translation": "/features/cells/src/main/res/values-%two_letters_code%/%original_file_name%"
33+
},
34+
{
35+
"source": "/features/meetings/src/main/res/values/strings.xml",
36+
"translation": "/features/meetings/src/main/res/values-%two_letters_code%/%original_file_name%"
37+
},
38+
{
39+
"source": "/features/sketch/src/main/res/values/strings.xml",
40+
"translation": "/features/sketch/src/main/res/values-%two_letters_code%/%original_file_name%"
41+
}
2842
]

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ android-junit5 = { module = "de.mannodermaus.gradle.plugins:android-junit5", ver
146146
grgit-core = { module = "org.ajoberstar.grgit:grgit-core", version.ref = "grgitCore" }
147147
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
148148
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "android-gradlePlugin" }
149+
android-gradleApi = { group = "com.android.tools.build", name = "gradle-api", version.ref = "android-gradlePlugin" }
149150
hilt-gradlePlugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" }
150151
googleGms-gradlePlugin = { module = "com.google.gms:google-services", version.ref = "google-gms" }
151152
googleGms-location = { module = "com.google.android.gms:play-services-location", version.ref = "gms-location" }

0 commit comments

Comments
 (0)