Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crowdin-compiler-plugin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/build
/bin
154 changes: 154 additions & 0 deletions crowdin-compiler-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
plugins {
id("org.jetbrains.kotlin.jvm")
id("java")
id("maven-publish")
alias(libs.plugins.buildconfig)
alias(libs.plugins.gradle.java.test.fixtures)
alias(libs.plugins.gradle.idea)
}

apply(from = rootProject.file("gradle/publishing.gradle.kts"))

sourceSets {
main {
java.setSrcDirs(listOf("src"))
resources.setSrcDirs(listOf("resources"))
}
testFixtures {
java.setSrcDirs(listOf("test-fixtures"))
}
test {
java.setSrcDirs(listOf("test", "test-gen"))
resources.setSrcDirs(listOf("testData"))
}
}

idea {
module.generatedSourceDirs.add(projectDir.resolve("test-gen"))
}

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlin {
jvmToolchain(17)
}

val annotationsRuntimeClasspath: Configuration by configurations.creating { isTransitive = false }

dependencies {
compileOnly(libs.kotlin.compiler)


testFixturesApi(libs.kotlin.test.junit5)
testFixturesApi(libs.kotlin.test.framework)
testFixturesApi(libs.kotlin.compiler)

// Dependencies required to run the internal test framework.
testRuntimeOnly(libs.junit)
testRuntimeOnly(libs.kotlin.reflect)
testRuntimeOnly(libs.kotlin.test)
testRuntimeOnly(libs.kotlin.script.runtime)
testRuntimeOnly(libs.kotlin.annotations.jvm)
}

buildConfig {
useKotlinOutput {
internalVisibility = true
}

packageName(group.toString())
buildConfigField("String", "KOTLIN_PLUGIN_ID", "\"${rootProject.group}\"")
}

tasks.test {
dependsOn(annotationsRuntimeClasspath)

useJUnitPlatform()
workingDir = rootDir

systemProperty("annotationsRuntime.classpath", annotationsRuntimeClasspath.asPath)

// Properties required to run the internal test framework.
setLibraryProperty("org.jetbrains.kotlin.test.kotlin-stdlib", "kotlin-stdlib")
setLibraryProperty("org.jetbrains.kotlin.test.kotlin-stdlib-jdk8", "kotlin-stdlib-jdk8")
setLibraryProperty("org.jetbrains.kotlin.test.kotlin-reflect", "kotlin-reflect")
setLibraryProperty("org.jetbrains.kotlin.test.kotlin-test", "kotlin-test")
setLibraryProperty("org.jetbrains.kotlin.test.kotlin-script-runtime", "kotlin-script-runtime")
setLibraryProperty("org.jetbrains.kotlin.test.kotlin-annotations-jvm", "kotlin-annotations-jvm")

systemProperty("idea.ignore.disabled.plugins", "true")
systemProperty("idea.home.path", rootDir)
}

kotlin {
jvmToolchain(17)

compilerOptions {
optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi")
optIn.add("org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI")
}
}

val generateTests by tasks.registering(JavaExec::class) {
inputs.dir(layout.projectDirectory.dir("testData"))
.withPropertyName("testData")
.withPathSensitivity(PathSensitivity.RELATIVE)
outputs.dir(layout.projectDirectory.dir("test-gen"))
.withPropertyName("generatedTests")

classpath = sourceSets.testFixtures.get().runtimeClasspath
mainClass.set("com.crowdin.platform.compiler.GenerateTestsKt")
workingDir = rootDir
}

tasks.compileTestKotlin {
dependsOn(generateTests)
}

fun Test.setLibraryProperty(propName: String, jarName: String) {
val path = project.configurations
.testRuntimeClasspath.get()
.files
.find { """$jarName-\d.*jar""".toRegex().matches(it.name) }
?.absolutePath
?: return
systemProperty(propName, path)
}

// Create sources JAR
val sourcesJar by tasks.registering(Jar::class) {
archiveClassifier.set("sources")
from(sourceSets.main.get().allSource)
}

publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
artifact(sourcesJar)

groupId = project.property("publishedGroupId") as String
artifactId = "compiler-plugin"
version = project.property("crowdinVersion") as String

val crowdinPublishing = project.extra["CrowdinPublishing"] as Any
val configureMethod = crowdinPublishing.javaClass.getMethod(
"configurePom",
org.gradle.api.publish.maven.MavenPublication::class.java,
Project::class.java,
String::class.java,
String::class.java
)
configureMethod.invoke(
crowdinPublishing,
this,
project,
"Crowdin Kotlin Compiler Plugin",
"Kotlin compiler plugin that transforms stringResource() calls to use Crowdin SDK for real-time translation updates in Jetpack Compose"
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.crowdin.platform.compiler.CrowdinCommandLineProcessor
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.crowdin.platform.compiler.CrowdinComponentRegistrar
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.crowdin.platform.compiler

import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption
import org.jetbrains.kotlin.compiler.plugin.CliOption
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
import org.jetbrains.kotlin.config.CompilerConfiguration

/**
* Command line processor for the Crowdin compiler plugin.
*
* This processes the command-line options passed to the compiler plugin
* and stores them in the compiler configuration.
*/
class CrowdinCommandLineProcessor : CommandLineProcessor {
override val pluginId: String = CrowdinComponentRegistrar.PLUGIN_ID

override val pluginOptions: Collection<AbstractCliOption> = listOf(
CliOption(
optionName = "enabled",
valueDescription = "true|false",
description = "Enable or disable the Crowdin compiler plugin",
required = false
)
)

override fun processOption(
option: AbstractCliOption,
value: String,
configuration: CompilerConfiguration
) {
when (option.optionName) {
"enabled" -> configuration.put(CrowdinComponentRegistrar.KEY_ENABLED, value)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.crowdin.platform.compiler

import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
import org.jetbrains.kotlin.config.CommonConfigurationKeys
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.CompilerConfigurationKey

/**
* Component registrar for the Crowdin compiler plugin.
*
* This is the entry point for the Kotlin compiler to discover and initialize
* our plugin. It registers the IR generation extension that performs the
* stringResource -> crowdinString transformation.
*/
class CrowdinComponentRegistrar : CompilerPluginRegistrar() {

companion object {
val KEY_ENABLED = CompilerConfigurationKey<String>("crowdin.enabled")
const val PLUGIN_ID = "com.crowdin.platform.compiler"
}

override val supportsK2: Boolean = true // Now using K2-compatible APIs

override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) {
val messageCollector = configuration.get(CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE)
// Check if the plugin is enabled (defaults to false for release builds)
val enabled = configuration.get(KEY_ENABLED, "false").toBoolean()

// Register our IR generation extension
IrGenerationExtension.registerExtension(CrowdinIrGenerationExtension(messageCollector, enabled))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.crowdin.platform.compiler

import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.types.classFqName
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid

/**
* IR generation extension that applies the Crowdin string resource transformer.
*
* This extension is invoked during the IR generation phase and applies our
* transformer to all IR elements in the module.
*/
class CrowdinIrGenerationExtension(
private val messageCollector: MessageCollector,
private val enabled: Boolean
) : IrGenerationExtension {

override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
if (!enabled) {
messageCollector.report(CompilerMessageSeverity.LOGGING, "[Crowdin IrGenerationExtension] Plugin disabled, skipping transformation")
return
}

// Check if Compose has already run
if (hasComposeTransformations(moduleFragment)) {
error(
"""
ERROR: Compose has already transformed the IR!
CrowdinGradlePlugin must run BEFORE Compose.

Make sure CrowdinGradlePlugin is applied before Compose:
plugins {
id("com.crowdin.platform.gradle")
id("org.jetbrains.kotlin.plugin.compose")
}
""".trimIndent()
)
}

messageCollector.report(CompilerMessageSeverity.LOGGING, "[Crowdin IrGenerationExtension] Applying transformer to module: ${moduleFragment.name}")

// Apply the transformer to all IR elements in the module
val transformer = CrowdinStringResourceTransformer(pluginContext, enabled)
moduleFragment.transformChildrenVoid(transformer)
}

private fun hasComposeTransformations(moduleFragment: IrModuleFragment): Boolean {
// Check for Compose-specific IR patterns
return moduleFragment.files.any { file ->
file.declarations.any { declaration ->
when (declaration) {
is IrFunction -> {
// Compose adds a Composer parameter
declaration.parameters.any { param ->
param.type.classFqName?.asString() ==
"androidx.compose.runtime.Composer"
}
}
else -> false
}
}
}
}
}
Loading
Loading