{"id":13607973,"url":"https://github.com/adrielcafe/lyricist","last_synced_at":"2025-10-04T18:34:55.746Z","repository":{"id":40758645,"uuid":"370198325","full_name":"adrielcafe/lyricist","owner":"adrielcafe","description":"🌎 The missing I18N/L10N (internationalization/localization) multiplatform library for Jetpack Compose!","archived":false,"fork":false,"pushed_at":"2024-07-05T03:59:24.000Z","size":317,"stargazers_count":738,"open_issues_count":15,"forks_count":19,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-04-19T16:20:09.520Z","etag":null,"topics":["android","android-library","compose","i18n","internationalization","jetpack-compose","kotlin","kotlin-android","kotlin-desktop","kotlin-library","kotlin-multiplatform","ksp","l10n","localization","string","strings","translation"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adrielcafe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"ko_fi":"adrielcafe"}},"created_at":"2021-05-24T01:50:05.000Z","updated_at":"2025-04-18T13:47:57.000Z","dependencies_parsed_at":"2024-01-15T15:46:29.327Z","dependency_job_id":"1fd43270-96f1-4c39-ba03-3e16e8d4db71","html_url":"https://github.com/adrielcafe/lyricist","commit_stats":{"total_commits":79,"total_committers":7,"mean_commits":"11.285714285714286","dds":"0.26582278481012656","last_synced_commit":"98985f6b905cb5337b72358b726f64f1fc90a684"},"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcafe%2Flyricist","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcafe%2Flyricist/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcafe%2Flyricist/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adrielcafe%2Flyricist/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adrielcafe","download_url":"https://codeload.github.com/adrielcafe/lyricist/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254544146,"owners_count":22088807,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","android-library","compose","i18n","internationalization","jetpack-compose","kotlin","kotlin-android","kotlin-desktop","kotlin-library","kotlin-multiplatform","ksp","l10n","localization","string","strings","translation"],"created_at":"2024-08-01T19:01:23.247Z","updated_at":"2025-10-04T18:34:55.605Z","avatar_url":"https://github.com/adrielcafe.png","language":"Kotlin","funding_links":["https://ko-fi.com/adrielcafe"],"categories":["Kotlin","Libraries","Multiplatform"],"sub_categories":["Testings","Android samples"],"readme":"[![Maven metadata URL](https://img.shields.io/maven-metadata/v?color=blue\u0026metadataUrl=https://s01.oss.sonatype.org/service/local/repo_groups/public/content/cafe/adriel/lyricist/lyricist/maven-metadata.xml\u0026style=for-the-badge)](https://repo.maven.apache.org/maven2/cafe/adriel/lyricist/)\n[![Android API](https://img.shields.io/badge/api-21%2B-brightgreen.svg?style=for-the-badge)](https://android-arsenal.com/api?level=21)\n[![kotlin](https://img.shields.io/github/languages/top/adrielcafe/lyricist.svg?style=for-the-badge\u0026color=blueviolet)](https://kotlinlang.org/)\n[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg?style=for-the-badge)](https://ktlint.github.io/)\n[![License MIT](https://img.shields.io/github/license/adrielcafe/lyricist.svg?style=for-the-badge\u0026color=orange)](https://opensource.org/licenses/MIT)\n\n# Lyricist 🌎🌍🌏 \n\u003e The missing [I18N and L10N](https://en.wikipedia.org/wiki/Internationalization_and_localization) multiplatform library for [Jetpack Compose](https://developer.android.com/jetpack/compose)!\n\nJetpack Compose greatly improved the way we build UIs on Android, but not how we **interact with strings**. `stringResource()` works well, but doesn't benefit from the idiomatic Kotlin like Compose.\n\nLyricist tries to make working with strings as powerful as building UIs with Compose, *i.e.*, working with parameterized string is now typesafe, use of `when` expression to work with plurals with more flexibility, and even load/update the strings dynamically via an API!\n\n#### Features\n- [x] Multiplatform: Android, Desktop, iOS, Web (JsCanvas)\n- [x] [Simple API](#usage) to handle locale changes and provide the current strings\n- [x] [Multi module support](#multi-module-settings)\n- [x] [Easy migration](#migrating-from-stringsxml) from `strings.xml`\n- [x] [Extensible](#extending-lyricist): supports Compose Multiplatform out of the box but can be integrated on any UI Toolkit\n- [x] Code generation with [KSP](https://github.com/google/ksp)\n\n#### Limitations\n* The XML processor doesn't handle `few` and `many` [plural values](https://developer.android.com/guide/topics/resources/string-resource#Plurals) (PRs are welcome) \n\n#### Why _Lyricist_?\nInspired by [accompanist](https://github.com/google/accompanist#why-the-name) library: music composing is done by a composer, and since this library is about writing ~~lyrics~~ strings, the role of a [lyricist](https://en.wikipedia.org/wiki/Lyricist) felt like a good name.\n\n## Usage\nTake a look at the [sample app](https://github.com/adrielcafe/lyricist/tree/main/sample/src/main/java/cafe/adriel/lyricist/sample) and [sample-multi-module](https://github.com/adrielcafe/lyricist/tree/main/sample-multi-module/src/main/java/cafe/adriel/lyricist/sample/multimodule) for working examples.\n\nStart by declaring your strings on a `data class`, `class` or `interface` (pick one). The strings can be anything (really, it's up to you): `Char`, `String`, `AnnotatedString`, `List\u003cString\u003e`, `Set\u003cString\u003e` or even lambdas!\n```kotlin\ndata class Strings(\n    val simple: String,\n    val annotated: AnnotatedString,\n    val parameter: (locale: String) -\u003e String,\n    val plural: (count: Int) -\u003e String,\n    val list: List\u003cString\u003e,\n    val nestedStrings: NestedStrings(),\n)\n\ndata class NestedStrings(\n    ...\n)\n```\n\nNext, create instances for each supported language and annotate with `@LyricistStrings`. The `languageTag` must be an [IETF BCP47](https://en.wikipedia.org/wiki/IETF_language_tag) compliant language tag ([docs](https://developer.android.com/guide/topics/resources/providing-resources#LocaleQualifier)). You must flag one of them as default.\n```kotlin\n@LyricistStrings(languageTag = Locales.EN, default = true)\nval EnStrings = Strings(\n    simple = \"Hello Compose!\",\n\n    annotated = buildAnnotatedString {\n        withStyle(SpanStyle(color = Color.Red)) { \n            append(\"Hello \") \n        }\n        withStyle(SpanStyle(fontWeight = FontWeight.Light)) { \n            append(\"Compose!\") \n        }\n    },\n\n    parameter = { locale -\u003e\n        \"Current locale: $locale\"\n    },\n\n    plural = { count -\u003e\n        val value = when (count) {\n            0 -\u003e \"no\"\n            1, 2 -\u003e \"a few\"\n            in 3..10 -\u003e \"a bunch of\"\n            else -\u003e \"a lot of\"\n        }\n        \"I have $value apples\"\n    },\n\n    list = listOf(\"Avocado\", \"Pineapple\", \"Plum\")\n)\n\n@LyricistStrings(languageTag = Locales.PT)\nval PtStrings = Strings(/* pt strings */)\n\n@LyricistStrings(languageTag = Locales.ES)\nval EsStrings = Strings(/* es strings */)\n\n@LyricistStrings(languageTag = Locales.RU)\nval RuStrings = Strings(/* ru strings */)\n```\n\nLyricist will generate the `LocalStrings` property, a [CompositionLocal](https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionLocal) that provides the strings of the current locale. It will also generate `rememberStrings()` and `ProvideStrings()`, call them to make `LocalStrings` accessible down the tree.\n```kotlin\nval lyricist = rememberStrings()\n\nProvideStrings(lyricist) {\n    // Content\n}\n\n// Or just \nProvideStrings {\n    // Content\n}\n```\n\nOptionally, you can specify the current and default (used as fallback) languages.\n```kotlin\nval lyricist = rememberStrings(\n    defaultLanguageTag = \"es-US\", // Default value is the one annotated with @LyricistStrings(default = true)\n    currentLanguageTag = getCurrentLanguageTagFromLocalStorage(),\n)\n```\n\nNow you can use `LocalStrings` to retrieve the current strings.\n```kotlin\nval strings = LocalStrings.current\n\nText(text = strings.simple)\n// \u003e Hello Compose!\n\nText(text = strings.annotated)\n// \u003e Hello Compose!\n\nText(text = strings.parameter(lyricist.languageTag))\n// \u003e Current locale: en\n\nText(text = strings.plural(1))\nText(text = strings.plural(5))\nText(text = strings.plural(20))\n// \u003e I have a few apples\n// \u003e I have a bunch of apples\n// \u003e I have a lot of apples\n\nText(text = strings.list.joinToString())\n// \u003e Avocado, Pineapple, Plum\n```\n\nUse the Lyricist instance provided by `rememberStrings()` to change the current locale. This will trigger a [recomposition](https://developer.android.com/jetpack/compose/mental-model#recomposition) that will update the entire content.\n```kotlin\nlyricist.languageTag = Locales.PT\n```\n\n**Important**\n\nLyricist uses the System locale as current language (on Compose it uses `Locale.current`). If your app has a mechanism to change the language in-app please set this value on `rememberStrings(currentLanguageTag = CURRENT_VALUE_HERE)`.\n\nIf you change the current language at runtime Lyricist won't persist the value on a local storage by itself, this should be done by you. You can save the current language tag on shared preferences, a local database or even through a remote API.\n\n### Controlling the visibility\nTo control the visibility (`public` or `internal`) of the generated code, provide the following (optional) argument to KSP in the module's `build.gradle`.\n```gradle\nksp {\n    arg(\"lyricist.internalVisibility\", \"true\")\n}\n```\n\n### Generating a `strings` helper property\nInstead of use `LocalStrings.current` to access your strings, you can simply call `strings`. Just provide the following (optional) argument to KSP in the module's `build.gradle`.\n```gradle\nksp {\n    arg(\"lyricist.generateStringsProperty\", \"true\")\n}\n```\nAfter a successfully build you can refactor your code as below. \n```kotlin\n// Before\nText(text = LocalStrings.current.hello)\n\n// After\nText(text = strings.hello)\n```\n\n## Multi module settings\n\nIf you are using Lyricist on a multi module project and the generated declarations (`LocalStrings`, `rememberStrings()`, `ProvideStrings()`) are too generic for you, provide the following (optional) arguments to KSP in the module's `build.gradle`.\n```gradle\nksp {\n    arg(\"lyricist.packageName\", \"com.my.app\")\n    arg(\"lyricist.moduleName\", project.name)\n}\n```\n\nLet's say you have a \"dashboard\" module, the generated declarations will be `LocalDashboardStrings`, `rememberDashboardStrings()` and `ProvideDashboardStrings()`.\n\n## Migrating from `strings.xml`\nSo you liked Lyricist, but already have a project with thousands of strings spread over multiples files? I have good news for you: Lyricist can extract these existing strings and generate all the code you just saw above.\nIf you don't want to have the Compose code generated by KSP, you can set the `lyricist.xml.generateComposeAccessors` arg to `\"false\"`, and you can write the code manually by following the instructions [below](#extending-lyricist).\n\nSimilar to the multi module setup, you must provide a few arguments to KSP. Lyricist will search for `strings.xml` files in the resources path. You can also provide a language tag to be used as default value for the `LocalStrings`. \n```gradle\nksp {\n    // Required\n    arg(\"lyricist.xml.resourcesPath\", android.sourceSets.main.res.srcDirs.first().absolutePath)\n    \n    // Optional\n    arg(\"lyricist.packageName\", \"com.my.app\")\n    arg(\"lyricist.xml.moduleName\", \"xml\")\n    arg(\"lyricist.xml.defaultLanguageTag\", \"en\")\n    arg(\"lyricist.xml.generateComposeAccessors\", \"false\")\n}\n```\n\nAfter the first build, the well-known `rememberStrings()` and `ProvideStrings()` (naming can vary depending on your KSP settings) will be available for use. Lyricist will also generated a `Locales` object containing all language tags currently in use in your project. \n```kotlin\nval lyricist = rememberStrings(strings)\n\nProvideStrings(lyricist, LocalStrings) {\n    // Content\n}\n\nlyricist.languageTag = Locales.PT\n```\n\nYou can easily migrate from `strings.xml` to Lyricist just by copying the generated files to your project. That way, you can finally say goodbye to `strings.xml`. \n\n## Extending Lyricist\n\n\u003cdetails\u003e\u003csummary\u003eWriting the generated code from KSP manually\u003c/summary\u003e\n\nDon't want to enable KSP to generate the code for you? No problem! Follow the steps below to integrate with Lyricist manually.\n\n1. Map each supported language tag to their corresponding instances.\n```kotlin\nval strings = mapOf(\n    Locales.EN to EnStrings,\n    Locales.PT to PtStrings,\n    Locales.ES to EsStrings,\n    Locales.RU to RuStrings\n)\n```\n\n2. Create your `LocalStrings` and choose one translation as default.\n```kotlin\nval LocalStrings = staticCompositionLocalOf { EnStrings }\n```\n\n3. Use the same functions, `rememberStrings()` and `ProvideStrings()`, to make your `LocalStrings` accessible down the tree. But this time you need to provide your `strings` and `LocalStrings` manually.\n```kotlin\nval lyricist = rememberStrings(strings)\n\nProvideStrings(lyricist, LocalStrings) {\n    // Content\n}\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eSupporting other UI Toolkits\u003c/summary\u003e\n\nAt the moment Lyricist only supports Jetpack Compose and Compose Multiplatform out of the box. If you need to use Lyricist with other UI Toolkit (Android Views, SwiftUI, Swing, GTK...) follow the instructions bellow.\n\n1. Map each supported language tag to their corresponding instances\n```kotlin\nval translations = mapOf(\n    Locales.EN to EnStrings,\n    Locales.PT to PtStrings,\n    Locales.ES to EsStrings,\n    Locales.RU to RuStrings\n)\n```\n\n2. Create an instance of Lyricist, can be a project-wide singleton or a local instance per module\n```kotlin\nval lyricist = Lyricist(defaultLanguageTag, translations)\n```\n\n3. Collect Lyricist state and notify the UI to update whenever it changes\n```kotlin\nlyricist.state.collect { (languageTag, strings) -\u003e\n    refreshUi(strings)\n}\n\n// Example for Compose\nval state by lyricist.state.collectAsState()\n\nCompositionLocalProvider(\n    LocalStrings provides state.strings\n) {\n    // Content\n}\n```\n\u003c/details\u003e\n\n## Troubleshooting\n\n\u003cdetails\u003e\u003csummary\u003eCan't use the generated code on my IDE\u003c/summary\u003e\n\nYou should set manually the source sets of the generated files, like described [here](https://github.com/google/ksp/issues/37).\n```gradle\nbuildTypes {\n    debug {\n        sourceSets {\n            main.java.srcDirs += 'build/generated/ksp/debug/kotlin/'\n        }\n    }\n    release {\n        sourceSets {\n            main.java.srcDirs += 'build/generated/ksp/release/kotlin/'\n        }\n    }\n}\n```\n\u003c/details\u003e\n\n## Import to your project\n\n1. Importing the [KSP plugin](https://github.com/google/ksp/blob/main/docs/quickstart.md#use-your-own-processor-in-a-project) in the project's `build.gradle` then apply to your module's `build.gradle`\n```kotlin\nplugins {\n    id(\"com.google.devtools.ksp\") version \"${ksp-latest-version}\"\n}\n```\n\n2. Add the desired dependencies to your module's `build.gradle`\n```kotlin\n// Required\nimplementation(\"cafe.adriel.lyricist:lyricist:${latest-version}\")\n\n// If you want to use @LyricistStrings to generate code for you\nksp(\"cafe.adriel.lyricist:lyricist-processor:${latest-version}\")\n\n// If you want to migrate from strings.xml\nksp(\"cafe.adriel.lyricist:lyricist-processor-xml:${latest-version}\")\n```\n\n#### Version Catalog\n```toml\n[versions]\nlyricist = {latest-version}\n\n[libraries]\nlyricist = { module = \"cafe.adriel.lyricist:lyricist\", version.ref = \"lyricist\" }\nlyricist-processor = { module = \"cafe.adriel.lyricist:lyricist-processor\", version.ref = \"lyricist\" }\nlyricist-processorXml = { module = \"cafe.adriel.lyricist:lyricist-processor-xml\", version.ref = \"lyricist\" }\n```\n\n#### Multiplatform setup\n\nDoing code generation only at `commonMain`. Currently workaround, for more information see [KSP Issue 567](https://github.com/google/ksp/issues/567)\n```kotlin\ndependencies {\n    add(\"kspCommonMainMetadata\", \"cafe.adriel.lyricist:lyricist-processor:${latest-version}\")\n}\n\ntasks.withType\u003corg.jetbrains.kotlin.gradle.dsl.KotlinCompile\u003c*\u003e\u003e().all {\n    if(name != \"kspCommonMainKotlinMetadata\") {\n        dependsOn(\"kspCommonMainKotlinMetadata\")\n    }\n}\n\nkotlin.sourceSets.commonMain {\n    kotlin.srcDir(\"build/generated/ksp/metadata/commonMain/kotlin\")\n}\n```\n\nCurrent version: ![Maven metadata URL](https://img.shields.io/maven-metadata/v?color=blue\u0026metadataUrl=https://s01.oss.sonatype.org/service/local/repo_groups/public/content/cafe/adriel/lyricist/lyricist/maven-metadata.xml)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadrielcafe%2Flyricist","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadrielcafe%2Flyricist","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadrielcafe%2Flyricist/lists"}