Lyricist

repository·main·Indexed 21 days ago

https://github.com/adrielcafe/lyricist

A multiplatform I18N and L10N library for Jetpack Compose that provides typesafe, idiomatic access to localized strings. It supports parameters, plurals, and dynamic updates using KSP for code generation. Lyricist allows defining strings in Kotlin classes or interfaces and includes a processor for migrating existing strings.xml files.

Tokens
3.2K
Snippets
13
Records
14
Agent score
24%

What's inside Lyricist

  1. Setup Lyricist for Kotlin Multiplatform (KMP)

    main

    Due to current KSP limitations, code generation for KMP must be performed at commonMain. Use the following configuration to apply the processor to commonMainMetadata and manually include the generated directory in your commonMain source set.

    dependencies {
        add("kspCommonMainMetadata", "cafe.adriel.lyricist:lyricist-processor:${latest-version}")
    }
    
    tasks.withType<org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>>().all {
        if(name != "kspCommonMainKotlinMetadata") {
            dependsOn("kspCommonMainKotlinMetadata")
        }
    }
    
    kotlin.sourceSets.commonMain {
        kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")
    }
  2. Configure Lyricist with Version Catalog

    main

    If you are using a Gradle Version Catalog (libs.versions.toml), add the following entries to manage Lyricist dependencies centrally:

    [versions]
    lyricist = {latest-version}
    
    [libraries]
    lyricist = { module = "cafe.adriel.lyricist:lyricist", version.ref = "lyricist" }
    lyricist-processor = { module = "cafe.adriel.lyricist:lyricist-processor", version.ref = "lyricist" }
    lyricist-processorXml = { module = "cafe.adriel.lyricist:lyricist-processor-xml", version.ref = "lyricist" }
  3. Run the multiplatform sample for different targets

    main

    The sample-multiplatform module provides Gradle tasks to run the application on various platforms. Use the following commands depending on your target environment:

    • MacOS Native (Desktop via Kotlin Native): Run using runNativeDebug.
    • JVM Native (Desktop): Run using the standard run task.
    • Web (Compose Canvas): Run using the jsBrowserDevelopmentRun task.
    • Android: Build the debug APK using assembleDebug or run directly via Android Studio.
    # MacOS Native (Desktop using Kotlin Native)
    ./gradlew :sample-multiplatform:runNativeDebug
    
    # JVM Native (Desktop)
    ./gradlew :sample-multiplatform:run
    
    # Web Compose Canvas
    ./gradlew :sample-multiplatform:jsBrowserDevelopmentRun
    
    # Building Android App
    ./gradlew :sample-multiplatform:assembleDebug
  4. Migrate from `strings.xml` to Lyricist

    main

    Lyricist can extract existing strings.xml files and generate the necessary Kotlin code. To perform a migration, configure KSP with the path to your resources and any desired naming customizations.

    If you want to use the extracted strings manually without KSP generating Compose accessors, set lyricist.xml.generateComposeAccessors to "false".

    ksp {
        // Required
        arg("lyricist.xml.resourcesPath", android.sourceSets.main.res.srcDirs.first().absolutePath)
        
        // Optional
        arg("lyricist.packageName", "com.my.app")
        arg("lyricist.xml.moduleName", "xml")
        arg("lyricist.xml.defaultLanguageTag", "en")
        arg("lyricist.xml.generateComposeAccessors", "false")
    }
  5. Define and use strings with Lyricist

    main

    To use Lyricist, define your strings in a data class, class, or interface. You can use various types like String, AnnotatedString, List<String>, or even lambdas for parameterized strings and plurals.

    For each language, create an instance of your string class and annotate it with @LyricistStrings. You must provide an IETF BCP47 compliant languageTag and mark one instance as default = true.

    Lyricist uses KSP to generate LocalStrings (a CompositionLocal), rememberStrings(), and ProvideStrings() to make your strings accessible in your Compose UI tree.

    // 1. Define the structure
    data class Strings(
        val simple: String,
        val parameter: (locale: String) -> String,
        val plural: (count: Int) -> String,
        val list: List<String>
    )
    
    // 2. Create language instances
    @LyricistStrings(languageTag = Locales.EN, default = true)
    val EnStrings = Strings(
        simple = "Hello Compose!",
        parameter = { locale -> "Current locale: $locale" },
        plural = { count -> 
            val value = if (count <= 2) "a few" else "a lot of"
            "I have $value apples"
        },
        list = listOf("Avocado", "Pineapple")
    )
    
    @LyricistStrings(languageTag = Locales.PT)
    val PtStrings = Strings(/* pt strings */)
    
    // 3. Provide strings in the UI tree
    val lyricist = rememberStrings()
    ProvideStrings(lyricist) {
        // Content
    }
    
    // 4. Access strings
    val strings = LocalStrings.current
    Text(text = strings.simple)
  6. Integrate Lyricist with non-Compose UI Toolkits

    main

    To use Lyricist in environments other than Jetpack Compose or Compose Multiplatform (e.g., Android Views, SwiftUI, Swing), you must manually manage the state and the mapping of translations.

    1. Create a map of language tags to your string instances.
    2. Instantiate Lyricist with a default language tag and your translations.
    3. Observe lyricist.state to react to language changes and update your UI manually.
    // 1. Map translations
    val translations = mapOf(
        Locales.EN to EnStrings,
        Locales.PT to PtStrings
    )
    
    // 2. Create Lyricist instance
    val lyricist = Lyricist("en", translations)
    
    // 3. Observe changes (Example for a generic UI)
    lyricist.state.collect { (languageTag, strings) ->
        refreshUi(strings)
    }
  7. Import Lyricist into your project

    main

    To use Lyricist, you must first apply the KSP plugin to your project and then add the appropriate Lyricist dependencies to your module's build.gradle.

    Depending on your needs, you may require the core library, the standard processor for @LyricistStrings, or the XML processor for migrating from strings.xml.

    // 1. Apply KSP plugin in project build.gradle
    plugins {
        id("com.google.devtools.ksp") version "${ksp-latest-version}"
    }
    
    // 2. Add dependencies in module build.gradle
    // Required
    implementation("cafe.adriel.lyricist:lyricist:${latest-version}")
    
    // For @LyricistStrings code generation
    ksp("cafe.adriel.lyricist:lyricist-processor:${latest-version}")
    
    // For migrating from strings.xml
    ksp("cafe.adriel.lyricist:lyricist-processor-xml:${latest-version}")
  8. Change the current locale at runtime

    main

    You can change the application's language dynamically by updating the languageTag property on the Lyricist instance returned by rememberStrings(). This triggers a recomposition that updates all strings in the UI tree.

    Note: Lyricist does not persist the language choice automatically. You must manually save the selected language tag to local storage (e.g., SharedPreferences or a database) and restore it using rememberStrings(currentLanguageTag = ...) when the app restarts.

    val lyricist = rememberStrings(
        currentLanguageTag = getCurrentLanguageTagFromLocalStorage()
    )
    
    // To change language:
    lyricist.languageTag = Locales.PT
  9. Configure KSP 2.0 for optimal performance

    main

    Lyricist is optimized for KSP 2.0. For better performance, K2 compiler compatibility, and incremental compilation (which can improve build speeds by 20-50%), add these settings to your gradle.properties file.

    # Enable KSP 2.0 architecture
    ksp.useKsp2=true
    
    # Enable incremental compilation
    ksp.incremental=true
    
    # Disable incremental compilation logging (set to true for debugging)
    ksp.incremental.log=false
  10. Configure KSP arguments for Lyricist

    main

    You can customize the code generation behavior of Lyricist by passing arguments to the KSP processor in your module's build.gradle file.

    Visibility Control

    To make the generated code internal instead of public, use: arg("lyricist.internalVisibility", "true")

    Helper Property

    To enable a shorthand strings property so you can call strings.hello instead of LocalStrings.current.hello, use: arg("lyricist.generateStringsProperty", "true")

    Multi-module Customization

    In multi-module projects, you can customize the names of the generated declarations (e.g., LocalDashboardStrings instead of LocalStrings) using packageName and moduleName: arg("lyricist.packageName", "com.my.app") arg("lyricist.moduleName", "dashboard")

    ksp {
        arg("lyricist.internalVisibility", "true")
        arg("lyricist.generateStringsProperty", "true")
        arg("lyricist.packageName", "com.my.app")
        arg("lyricist.moduleName", "dashboard")
    }