Skip to content

Commit b6d8940

Browse files
committed
feat: decouple API from Compose
1 parent 16ac28c commit b6d8940

11 files changed

Lines changed: 197 additions & 89 deletions

File tree

README.md

Lines changed: 97 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Lyricist tries to make working with strings as powerful as building UIs with Com
1616
- [x] [Simple API](#usage) to handle locale changes and provide the current strings
1717
- [x] [Multi module support](#multi-module-settings)
1818
- [x] [Easy migration](#migrating-from-stringsxml) from `strings.xml`
19+
- [x] [Extensible](#extending-lyricist): supports Compose Multiplatform out of the box but can be integrated on any UI Toolkit
1920
- [x] Code generation with [KSP](https://github.com/google/ksp)
2021

2122
#### Limitations
@@ -99,37 +100,14 @@ ProvideStrings {
99100
}
100101
```
101102

102-
<details><summary>Writing the code for yourself</summary>
103-
104-
Don't want to enable KSP to generate the code for you? No problem! Follow the steps below to integrate with Lyricist manually.
105-
106-
First, map each supported language tag to their corresponding instances.
103+
Optionally, you can specify the current and default (used as fallback) languages.
107104
```kotlin
108-
val strings = mapOf(
109-
Locales.EN to EnStrings,
110-
Locales.PT to PtStrings,
111-
Locales.ES to EsStrings,
112-
Locales.RU to RuStrings
105+
val lyricist = rememberStrings(
106+
defaultLanguageTag = "es-US", // Default value is the one annotated with @LyricistStrings(default = true)
107+
currentLanguageTag = getCurrentLanguageTagFromLocalStorage(),
113108
)
114109
```
115110

116-
Next, create your `LocalStrings` and choose one translation as default.
117-
```kotlin
118-
val LocalStrings = staticCompositionLocalOf { EnStrings }
119-
```
120-
121-
Finally, 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.
122-
```kotlin
123-
val lyricist = rememberStrings(strings)
124-
125-
ProvideStrings(lyricist, LocalStrings) {
126-
// Content
127-
}
128-
```
129-
130-
---
131-
</details>
132-
133111
Now you can use `LocalStrings` to retrieve the current strings.
134112
```kotlin
135113
val strings = LocalStrings.current
@@ -154,11 +132,17 @@ Text(text = strings.list.joinToString())
154132
// > Avocado, Pineapple, Plum
155133
```
156134

157-
Use 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 strings wherever they are being used.
135+
Use 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.
158136
```kotlin
159137
lyricist.languageTag = Locales.PT
160138
```
161139

140+
**Important**
141+
142+
Lyricist 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)`.
143+
144+
If 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.
145+
162146
### Controlling the visibility
163147
To control the visibility (`public` or `internal`) of the generated code, provide the following (optional) argument to KSP in the module's `build.gradle`.
164148
```gradle
@@ -174,8 +158,14 @@ ksp {
174158
arg("lyricist.generateStringsProperty", "true")
175159
}
176160
```
161+
After a successfully build you can refactor your code as below.
162+
```kotlin
163+
// Before
164+
Text(text = LocalStrings.current.hello)
177165

178-
**Important:** Lyricist uses the System locale as default. It won't persist the current locale on storage, is outside its scope.
166+
// After
167+
Text(text = strings.hello)
168+
```
179169

180170
## Multi module settings
181171

@@ -218,6 +208,73 @@ lyricist.languageTag = Locales.PT
218208

219209
You 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`.
220210

211+
## Extending Lyricist
212+
213+
<details><summary>Writing the generated code from KSP manually</summary>
214+
215+
Don't want to enable KSP to generate the code for you? No problem! Follow the steps below to integrate with Lyricist manually.
216+
217+
1. Map each supported language tag to their corresponding instances.
218+
```kotlin
219+
val strings = mapOf(
220+
Locales.EN to EnStrings,
221+
Locales.PT to PtStrings,
222+
Locales.ES to EsStrings,
223+
Locales.RU to RuStrings
224+
)
225+
```
226+
227+
2. Create your `LocalStrings` and choose one translation as default.
228+
```kotlin
229+
val LocalStrings = staticCompositionLocalOf { EnStrings }
230+
```
231+
232+
3. 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.
233+
```kotlin
234+
val lyricist = rememberStrings(strings)
235+
236+
ProvideStrings(lyricist, LocalStrings) {
237+
// Content
238+
}
239+
```
240+
</details>
241+
242+
<details><summary>Supporting other UI Toolkits</summary>
243+
244+
At 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.
245+
246+
1. Map each supported language tag to their corresponding instances
247+
```kotlin
248+
val translations = mapOf(
249+
Locales.EN to EnStrings,
250+
Locales.PT to PtStrings,
251+
Locales.ES to EsStrings,
252+
Locales.RU to RuStrings
253+
)
254+
```
255+
256+
2. Create an instance of Lyricist, can be a project-wide singleton or a local instance per module
257+
```kotlin
258+
val lyricist = Lyricist(defaultLanguageTag, translations)
259+
```
260+
261+
3. Collect Lyricist state and notify the UI to update whenever it changes
262+
```kotlin
263+
lyricist.state.collect { (languageTag, strings) ->
264+
refreshUi(strings)
265+
}
266+
267+
// Example for Compose
268+
val state by lyricist.state.collectAsState()
269+
270+
CompositionLocalProvider(
271+
LocalStrings provides state.strings
272+
) {
273+
// Content
274+
}
275+
```
276+
</details>
277+
221278
## Troubleshooting
222279

223280
<details><summary>Can't use the generated code on my IDE</summary>
@@ -260,6 +317,17 @@ ksp("cafe.adriel.lyricist:lyricist-processor:${latest-version}")
260317
ksp("cafe.adriel.lyricist:lyricist-processor-xml:${latest-version}")
261318
```
262319

320+
#### Version Catalog
321+
```toml
322+
[versions]
323+
lyricist = {latest-version}
324+
325+
[libraries]
326+
lyricist = { module = "cafe.adriel.lyricist:lyricist", version.ref = "lyricist" }
327+
lyricist-processor = { module = "cafe.adriel.lyricist:lyricist-processor", version.ref = "lyricist" }
328+
lyricist-processorXml = { module = "cafe.adriel.lyricist:lyricist-processor-xml", version.ref = "lyricist" }
329+
```
330+
263331
#### Multiplatform setup
264332

265333
Doing code generation only at `commonMain`. Currently workaround, for more information see [KSP Issue 567](https://github.com/google/ksp/issues/567)
@@ -279,15 +347,4 @@ kotlin.sourceSets.commonMain {
279347
}
280348
```
281349

282-
#### Version Catalog
283-
```toml
284-
[versions]
285-
lyricist = {latest-version}
286-
287-
[libraries]
288-
lyricist-library = { module = "cafe.adriel.lyricist:lyricist", version.ref = "lyricist" }
289-
lyricist-processor = { module = "cafe.adriel.lyricist:lyricist-processor", version.ref = "lyricist" }
290-
lyricist-processorXml = { module = "cafe.adriel.lyricist:lyricist-processor-xml", version.ref = "lyricist" }
291-
```
292-
293350
Current version: ![Maven metadata URL](https://img.shields.io/maven-metadata/v?color=blue&metadataUrl=https://s01.oss.sonatype.org/service/local/repo_groups/public/content/cafe/adriel/lyricist/lyricist/maven-metadata.xml)

buildSrc/src/main/kotlin/Setup.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,4 @@ private fun KotlinJvmOptions.configureKotlinJvmOptions(
111111

112112
private fun Project.findAndroidExtension(): BaseExtension = extensions.findByType<LibraryExtension>()
113113
?: extensions.findByType<com.android.build.gradle.AppExtension>()
114-
?: error("Could not found Android application or library plugin applied on module $name")
114+
?: error("Could not find Android application or library plugin applied on module $name")

gradle/libs.versions.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ plugin-maven = "0.22.0"
55

66
kotlin = "1.9.10"
77

8+
coroutines = "1.7.3"
89
ksp = "1.9.10-1.0.13"
910
caseFormat = "0.2.0"
1011
konsumeXml = "1.0"
@@ -25,6 +26,7 @@ plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.
2526
plugin-ksp = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
2627
plugin-compose-multiplatform = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "composeMultiplatform" }
2728

29+
coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
2830
ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
2931
caseFormat = { module = "com.fleshgrinder.kotlin:case-format", version.ref = "caseFormat" }
3032
konsumeXml = { module = "com.gitlab.mvysny.konsume-xml:konsume-xml", version.ref = "konsumeXml" }
@@ -38,4 +40,11 @@ compose-material = { module = "androidx.compose.material:material", version.ref
3840
test-junit = { module = "androidx.test.ext:junit-ktx", version.ref = "test-junit" }
3941

4042
[bundles]
41-
plugins = ["plugin-android", "plugin-ktlint", "plugin-maven", "plugin-kotlin", "plugin-ksp", "plugin-compose-multiplatform"]
43+
plugins = [
44+
"plugin-android",
45+
"plugin-ktlint",
46+
"plugin-maven",
47+
"plugin-kotlin",
48+
"plugin-ksp",
49+
"plugin-compose-multiplatform",
50+
]

lyricist-compose/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ kotlin {
1616
val commonMain by getting {
1717
dependencies {
1818
api(project(":lyricist-core"))
19+
implementation(libs.coroutines)
1920
compileOnly(compose.runtime)
2021
compileOnly(compose.ui)
2122
}

lyricist-compose/src/commonMain/kotlin/cafe/adriel/lyricist/Lyricist.kt

Lines changed: 0 additions & 28 deletions
This file was deleted.

lyricist-compose/src/commonMain/kotlin/cafe/adriel/lyricist/LyricistUtils.kt renamed to lyricist-compose/src/commonMain/kotlin/cafe/adriel/lyricist/LyricistCompose.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,35 @@ package cafe.adriel.lyricist
22

33
import androidx.compose.runtime.Composable
44
import androidx.compose.runtime.CompositionLocalProvider
5+
import androidx.compose.runtime.LaunchedEffect
56
import androidx.compose.runtime.ProvidableCompositionLocal
7+
import androidx.compose.runtime.collectAsState
8+
import androidx.compose.runtime.getValue
69
import androidx.compose.runtime.remember
710
import androidx.compose.ui.text.intl.Locale
811

912
@Composable
1013
public fun <T> rememberStrings(
1114
translations: Map<LanguageTag, T>,
12-
languageTag: LanguageTag = Locale.current.toLanguageTag()
15+
defaultLanguageTag: LanguageTag = "en",
16+
currentLanguageTag: LanguageTag = Locale.current.toLanguageTag()
1317
): Lyricist<T> =
14-
remember { Lyricist(languageTag, translations) }
18+
remember(defaultLanguageTag) {
19+
Lyricist(defaultLanguageTag, translations)
20+
}.apply {
21+
languageTag = currentLanguageTag
22+
}
1523

1624
@Composable
1725
public fun <T> ProvideStrings(
1826
lyricist: Lyricist<T>,
1927
provider: ProvidableCompositionLocal<T>,
2028
content: @Composable () -> Unit
2129
) {
30+
val state by lyricist.state.collectAsState()
31+
2232
CompositionLocalProvider(
23-
provider provides lyricist.strings,
33+
provider provides state.strings,
2434
content = content
2535
)
2636
}

lyricist-core/build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,13 @@ android {
99
}
1010

1111
kotlinMultiplatform()
12+
13+
kotlin {
14+
sourceSets {
15+
val commonMain by getting {
16+
dependencies {
17+
implementation(libs.coroutines)
18+
}
19+
}
20+
}
21+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package cafe.adriel.lyricist
2+
3+
import kotlinx.coroutines.flow.MutableStateFlow
4+
import kotlinx.coroutines.flow.StateFlow
5+
import kotlinx.coroutines.flow.asStateFlow
6+
7+
public typealias LanguageTag = String
8+
9+
public class Lyricist<T>(
10+
private val defaultLanguageTag: LanguageTag,
11+
private val translations: Map<LanguageTag, T>
12+
) {
13+
14+
private val mutableState: MutableStateFlow<LyricistState<T>> =
15+
MutableStateFlow(LyricistState(defaultLanguageTag, getStrings(defaultLanguageTag)))
16+
17+
public val state: StateFlow<LyricistState<T>> =
18+
mutableState.asStateFlow()
19+
20+
public var languageTag: LanguageTag
21+
get() = mutableState.value.languageTag
22+
set(languageTag) {
23+
mutableState.value = LyricistState(languageTag, getStrings(languageTag))
24+
}
25+
26+
public val strings: T
27+
get() = mutableState.value.strings
28+
29+
private val LanguageTag.fallback: LanguageTag
30+
get() = split(FALLBACK_REGEX).first()
31+
32+
private fun getStrings(languageTag: LanguageTag) =
33+
translations[languageTag]
34+
?: translations[languageTag.fallback]
35+
?: translations[defaultLanguageTag]
36+
?: throw LyricistException("Strings for language tag $languageTag not found")
37+
38+
private companion object {
39+
private val FALLBACK_REGEX = Regex("[-_]")
40+
}
41+
}
42+
43+
public data class LyricistState<T> internal constructor(
44+
val languageTag: LanguageTag,
45+
val strings: T,
46+
)
47+
48+
public class LyricistException internal constructor(
49+
override val message: String
50+
) : RuntimeException()
51+
52+
@Target(AnnotationTarget.PROPERTY)
53+
@Retention(AnnotationRetention.SOURCE)
54+
public annotation class LyricistStrings(
55+
val languageTag: LanguageTag,
56+
val default: Boolean = false
57+
)

lyricist-core/src/commonMain/kotlin/cafe.adriel.lyricist/LyricistStrings.kt

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)