diff --git a/.junie/guidelines.md b/.junie/guidelines.md new file mode 100644 index 00000000..ad201871 --- /dev/null +++ b/.junie/guidelines.md @@ -0,0 +1,158 @@ +# Development Guidelines for Tolgee Mobile Kotlin SDK + +This document provides essential information for developers working on the Tolgee Mobile Kotlin SDK project. + +## Build/Configuration Instructions + +### Project Structure + +The project consists of several modules: + +- **core**: The Kotlin Multiplatform library providing runtime support for Tolgee translations +- **compose**: Compose Multiplatform integration for Tolgee +- **compiler-plugin**: Kotlin compiler plugin for Tolgee +- **gradle-plugin**: Gradle plugin for integrating Tolgee into projects +- **demo**: Demo applications showcasing Tolgee usage + +### Building the Project + +1. **Prerequisites**: + - JDK 11 or higher + - Kotlin 1.9.0 or higher + - Android SDK (for Android targets) + - Xcode (for Apple targets) + +2. **Building from the command line**: + ```bash + ./gradlew build + ``` + +3. **Building specific modules**: + ```bash + ./gradlew :core:build + ./gradlew :compose:build + ./gradlew :gradle-plugin:build + ./gradlew :compiler-plugin:build + ``` + +4. **Publishing to Maven Local** (for local testing): + ```bash + ./gradlew publishToMavenLocal + ``` + You may need to disable signing temporarily as it is enforced by default. + +## Testing Information + +Testing infrastructure is in a relatively ok state, but there are no tests at the moment. + +### Running Tests + +Tests can be run using the Gradle test task: + +```bash +# Run all tests +./gradlew test + +# Run tests for a specific module +./gradlew :gradle-plugin:test +./gradlew :core:test +./gradlew :compose:test +./gradlew :compiler-plugin:test + +# Run a specific test class +./gradlew :gradle-plugin:test --tests "TolgeeTest" + +# Run a specific test method +./gradlew :gradle-plugin:test --tests "TolgeeTest.tolgee cli version check" +``` + +### Adding New Tests + +1. **Create a test file** in the appropriate module's test directory: + - For JVM modules: `/src/test/kotlin/` + - For Android modules: `/src/androidTest/kotlin/` + - For multiplatform modules: `/src/commonTest/kotlin/` (or platform-specific test directories) + +2. **Test structure example**: + +```kotlin +class MyTest { + + @Test + fun `test some functionality`() { + // Test implementation + assertEquals(expected, actual, "Error message") + } +} +``` + +3. **Running your new test**: + ```bash + ./gradlew ::test --tests "MyTest" + ``` + +### Test Example + +Here's a simple test example that was created and verified to work: + +```kotlin +class SimpleTest { + + @Test + fun `simple addition test`() { + // A simple test to demonstrate testing in this project + assertEquals(4, 2 + 2, "Basic addition should work correctly") + } +} +``` + +## Additional Development Information + +### Code Style + +- The project follows Kotlin coding conventions +- Use 4 spaces for indentation +- The maximum line length is 120 characters (not fully enforced at the moment) +- Use trailing commas in parameter lists and collection literals that span multiple lines + +### Multiplatform Considerations + +- Common code should be placed in `commonMain` source sets +- Platform-specific code should be placed in the appropriate platform-specific source sets: + - `androidMain` for Android + - `jvmMain` for JVM + - `appleMain` for Apple platforms (iOS, macOS, etc.) + - `jsMain` for JavaScript + - etc. + +### Dependency Management + +- Dependencies are managed through Version Catalog in `gradle/libs.versions.toml` +- When adding new dependencies, add them to the Version Catalog rather than directly in build scripts + +### Publishing + +The project uses the Vanniktech Maven Publish plugin for publishing to Maven Central: + +```kotlin +mavenPublishing { + publishToMavenCentral(host = SonatypeHost.CENTRAL_PORTAL, automaticRelease = true) + signAllPublications() + + coordinates( + groupId = "io.tolgee", + artifactId = "module-name", + version = "version" + ) +} +``` + +### API Compatibility + +The project uses the Binary Compatibility Validator plugin to ensure API compatibility. After making changes to public APIs, run: + +```bash +./gradlew apiDump +``` + +This will update the API dump files in the `api` directory of each module. These files should be committed to the repository. diff --git a/README.md b/README.md index cfbe1372..197224fe 100644 --- a/README.md +++ b/README.md @@ -1,138 +1,159 @@ -# Kotlin Multiplatform and Android package for Tolgee +# Tolgee Mobile Kotlin SDK (Alpha) 🐁 -🚨🚨🚨This package is currently under heavy development and will be released under alpha versions until stable and properly tested by pilot users.🚨🚨🚨 +[![Tolgee](https://img.shields.io/badge/Tolgee-f06695)](https://tolgee.io/) +![Android](https://img.shields.io/badge/Android-Supported-green?logo=android) +![Jetpack Compose](https://img.shields.io/badge/Jetpack%20Compose-Supported-green?logo=jetpackcompose) +![Compose Multiplatform](https://img.shields.io/badge/Compose%20Multiplatform-Supported-green?logo=kotlin) +![language](https://img.shields.io/github/languages/top/tolgee/tolgee-mobile-kotlin-sdk) +[![github release](https://img.shields.io/github/v/release/tolgee/tolgee-mobile-kotlin-sdk?label=GitHub%20Release)](https://github.com/tolgee/tolgee-mobile-kotlin-sdk/releases/latest) +[![licence](https://img.shields.io/badge/license-Apache%202%20-blue)](https://github.com/tolgee/tolgee-mobile-kotlin-sdk/blob/master/LICENSE) +[![github stars](https://img.shields.io/github/stars/tolgee/tolgee-mobile-kotlin-sdk?style=social&label=Tolgee%20Mobile%20Kotlin%20SDK)](https://github.com/tolgee/tolgee-mobile-kotlin-sdk) +[![github stars](https://img.shields.io/github/stars/tolgee/tolgee-platform?style=social&label=Tolgee%20Platform)](https://github.com/tolgee/tolgee-platform) +[![Github discussions](https://img.shields.io/github/discussions/tolgee/tolgee-platform)](https://github.com/tolgee/tolgee-platform/discussions) +[![Dev.to](https://img.shields.io/badge/Dev.to-tolgee_i18n?logo=devdotto&logoColor=white)](https://dev.to/tolgee_i18n) +[![Read the Docs](https://img.shields.io/badge/Read%20the%20Docs-8CA1AF?logo=readthedocs&logoColor=fff)](https://docs.tolgee.io/) +[![Slack](https://img.shields.io/badge/Slack-4A154B?logo=slack&logoColor=fff)](https://tolg.ee/slack) +[![YouTube](https://img.shields.io/badge/YouTube-%23FF0000.svg?logo=YouTube&logoColor=white)](https://www.youtube.com/@tolgee) +[![LinkedIn](https://custom-icon-badges.demolab.com/badge/LinkedIn-0A66C2?logo=linkedin-white&logoColor=fff)](https://www.linkedin.com/company/tolgee/) +[![X](https://img.shields.io/badge/X-%23000000.svg?logo=X&logoColor=white)](https://x.com/Tolgee_i18n) -[![Tolgee](https://img.shields.io/badge/Tolgee-f06695?style=for-the-badge)](https://tolgee.io/) ![Compose Multiplatform](https://img.shields.io/badge/Compose%20Multiplatform-Supported-green?style=for-the-badge) ![Jetpack Compose](https://img.shields.io/badge/Jetpack%20Compose-Supported-green?style=for-the-badge) +## What is Tolgee? -A flexible Gradle plugin and runtime library for integrating [Tolgee translations](https://tolgee.io) into **Kotlin Multiplatform** and **Compose** projects. +[Tolgee](https://tolgee.io/) is a powerful localization platform that simplifies the translation process for your applications. +This SDK provides integration for Kotlin-based projects, with a primary focus on Android. -## Gradle plugin +Currently, Android is fully supported, but any Kotlin-based codebase can in theory use this library. -Comes with a convenient task to pull your latest translations directly into your resources folder. +## Features -### Setup +- **Over-the-air updates**: Update your translations without releasing a new app version +- **Multiple format support**: + - Sprintf (Android SDK) formatting + - ICU (Tolgee Native Flat JSON) formatting +- **Compose integration**: Full integration with Jetpack Compose and Compose Multiplatform +- **Compiler plugin**: Automatically transform existing code to use Tolgee without manual changes +- **Kotlin Multiplatform**: Designed with multiplatform support in mind -Using Version Catalog is highly recommended to keep your versions aligned. - -```toml -[plugins] -tolgee = { id = "dev.datlag.tolgee", version.ref = "tolgee" } -``` - -**Configuration** +## Modules -You can change the plugin behavior to your needs: - -```kotlin -tolgee { - // REQUIRED - apiKey.set("") // or use the 'tolgee.apikey=' property instead - - // more options - pull { ... } - push { ... } - - // change compile time behavior - compilerPlugin { - android { - // Replaces Context.getString occurrences with Context.getStringInstant - replaceGetString.set(false) // default true - } - } -} -``` +The SDK is split into multiple modules, each serving a specific purpose: -### Usage +- **[Core](./core/README.md)**: Base library for fetching translations from CDN and querying them +- **[Compose](./compose/README.md)**: Extension for using the library with Jetpack Compose or Compose Multiplatform +- **[Gradle Plugin](./gradle-plugin/README.md)**: Gradle plugin for integrating and configuring the compiler plugin -Pull translations from Tolgee using the `pullTranslation` Gradle task. -Push local translations to Tolgee using the `pushTranslation` Gradle task. +## Which Module Should I Use? -## Core +- If you are using **traditional Android Views**, use the [Core](./core/README.md) module +- If you are using **Jetpack Compose** or **Compose Multiplatform**, use the [Compose](./compose/README.md) module +- If you want to **automatically transform existing code** to use Tolgee, add the [Gradle Plugin](./gradle-plugin/README.md) -This Kotlin Multiplatform library provides runtime support for Tolgee translations in your app. -No longer creating a new release just to update your strings. +## Installation -### Setup +> [!NOTE] +> For managing static translations (used as fallback), check out [tolgee-cli](https://github.com/tolgee/tolgee-cli). +> It provides tools for updating and syncing your static translation files. +> +> In each demo project you can find an example of `.tolgeerc` configuration file. Using Version Catalog is highly recommended to keep your versions aligned. +### Core Module (Traditional Android) + ```toml +# gradle/libs.versions.toml [libraries] -tolgee = { group = "dev.datlag.tolgee", name = "core", version.ref = "tolgee" } +tolgee = { group = "io.tolgee.mobile-kotlin-sdk", name = "core", version.ref = "tolgee" } ``` -### Usage - -Simply create a `Tolgee` singleton or multiple instances, using an API Key and/or a content delivery url. +```kotlin +// build.gradle.kts +dependencies { + implementation(libs.tolgee) +} +``` -#### Content Delivery +### Compose Module (Jetpack Compose or Compose Multiplatform) -Content Delivery supports JSON only and can be used with any formatting option. +```toml +# gradle/libs.versions.toml +[libraries] +tolgee = { group = "io.tolgee.mobile-kotlin-sdk", name = "compose", version.ref = "tolgee" } +``` ```kotlin -/** Thread safe: Retrieve the current singleton or create one. */ -val tolgee = Tolgee.instanceOrInit { - apiKey = "" - contentDelivery("") { - format(Tolgee.Formatter.ICU) // default formatting - format(Tolgee.Formatter.Sprintf) // for sprintf or Java.format formatting - } +// build.gradle.kts +dependencies { + implementation(libs.tolgee) } +``` -/** Updates the text automatically when loaded from API or locale changed. */ -val updatingText: Flow = tolgee.translation("key") +### Gradle Plugin (Compiler Plugin) -/** Returns the text that's currently loaded from API. */ -/** Requires `tolgee.preload` or `tolgee.translation` call else always null. */ -val currentText: String? = tolgee.instant("key") +```toml +# gradle/libs.versions.toml +[plugins] +tolgee = { id = "io.tolgee.mobile-kotlin-sdk", version.ref = "tolgee" } ``` -## Compose +```kotlin +// build.gradle.kts +plugins { + alias(libs.plugins.tolgee) +} +``` -### Setup +## Basic Usage -Using Version Catalog is highly recommended to keep your versions aligned. +For detailed usage instructions, please refer to the module-specific documentation: -```toml -[libraries] -tolgee = { group = "dev.datlag.tolgee", name = "compose", version.ref = "tolgee" } -``` +- [Core Module Documentation](./core/README.md)—For traditional Android and base functionality +- [Compose Module Documentation](./compose/README.md)—For Jetpack Compose and Compose Multiplatform +- [Gradle Plugin Documentation](./gradle-plugin/README.md)—For compiler plugin configuration -### Usage +### Quick Start -``` -@Composable -fun SimpleText() { - Text(text = stringResource(tolgee, Res.string.about)) -} +Here's a quick example of initializing Tolgee in an Android application: -@Composable -fun ArgsSupported(vararg args: Any) { - Text(text = stringResource(tolgee, Res.string.about, *args)) +```kotlin +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + + Tolgee.init { + contentDelivery { + url = "https://cdn.tolg.ee/your-cdn-url-prefix" + storage = TolgeeStorageProviderAndroid(this@MyApplication, BuildConfig.VERSION_CODE) + } + } + } } ``` -#### Jetpack Compose? +## Example Projects -What if you are using Jetpack Compose (Android only) or some explicit strings in your android source? -No problem! This is handled as well. +For complete examples of how to use the Tolgee SDK, check out the demo projects: -```kotlin -@Composable -fun AndroidOnly() { - Text(text = i18n.stringResource(R.string.android_string)) -} +- [Example Android](./demo/exampleandroid)—Traditional Android Views example +- [Example Jetpack](./demo/examplejetpack)—Jetpack Compose example +- [Multiplatform Compose](./demo/multiplatform-compose)—Compose Multiplatform example -@Composable -fun AndroidWithArgs(vararg args: Any) { - Text(text = i18n.stringResource(R.string.android_string, *args)) -} -``` +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. +Check out [guidelines.md](.junie/guidelines.md) for some information about the project internals and information about the workflow. + +## License + +This project is licensed under the Apache License 2.0—see the [LICENSE](LICENSE) file for details. + +## Contributors -### This is a non-profit project! + + contributors + -Sponsoring to this project means sponsoring to all my projects! -So the further text is not to be attributed to this project, but to all my apps and libraries. +Let us know what you think! #FeedbackWanted ❤️ -Supporting this project helps to keep it up-to-date. You can donate if you want or contribute to the project as well. -This shows that the library is used by people, and it's worth to maintain. +---- +🧀 diff --git a/build.gradle.kts b/build.gradle.kts index 2c072242..a955d1cf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,8 +22,9 @@ plugins { // Ignore API on demo projects apiValidation { ignoredProjects.add("demo") - ignoredProjects.add("android-view") ignoredProjects.add("multiplatform-compose") + ignoredProjects.add("exampleandroid") + ignoredProjects.add("examplejetpack") } dependencies { diff --git a/compiler-plugin/api/compiler-plugin.api b/compiler-plugin/api/compiler-plugin.api index 90049e44..e89f5cf4 100644 --- a/compiler-plugin/api/compiler-plugin.api +++ b/compiler-plugin/api/compiler-plugin.api @@ -8,6 +8,8 @@ public final class io/tolgee/TolgeeCommandLineProcessor : org/jetbrains/kotlin/c public final class io/tolgee/TolgeeCommandLineProcessor$Companion { public final fun getKEY_ANDROID_GET_STRING_ENABLED ()Lorg/jetbrains/kotlin/config/CompilerConfigurationKey; + public final fun getKEY_ANDROID_PLURAL_STRING_ENABLED ()Lorg/jetbrains/kotlin/config/CompilerConfigurationKey; + public final fun getKEY_COMPOSE_PLURAL_STRING_ENABLED ()Lorg/jetbrains/kotlin/config/CompilerConfigurationKey; public final fun getKEY_COMPOSE_STRING_RESOURCE_ENABLED ()Lorg/jetbrains/kotlin/config/CompilerConfigurationKey; } diff --git a/compiler-plugin/build.gradle.kts b/compiler-plugin/build.gradle.kts index e940982d..cc4ccde4 100644 --- a/compiler-plugin/build.gradle.kts +++ b/compiler-plugin/build.gradle.kts @@ -9,7 +9,7 @@ plugins { alias(libs.plugins.vanniktech.publish) } -val libGroup = "io.tolgee" +val libGroup = "io.tolgee.mobile-kotlin-sdk" val libName = "compiler-plugin" group = libGroup @@ -20,7 +20,7 @@ dokka { dokkaSourceSets.configureEach { sourceLink { localDirectory.set(file("src")) - remoteUrl("https://github.com/tolgee/compose-tolgee/tree/master/compiler-plugin/src") + remoteUrl("https://github.com/tolgee/tolgee-mobile-kotlin-sdk/tree/master/compiler-plugin/src") } } } @@ -46,7 +46,7 @@ mavenPublishing { name.set(libName) description.set("Compiler plugin for Tolgee translations.") - url.set("https://github.com/tolgee/compose-tolgee") + url.set("https://github.com/tolgee/tolgee-mobile-kotlin-sdk") licenses { license { @@ -56,8 +56,8 @@ mavenPublishing { } scm { - url.set("https://github.com/tolgee/compose-tolgee") - connection.set("scm:git:git://github.com/tolgee/compose-tolgee.git") + url.set("https://github.com/tolgee/tolgee-mobile-kotlin-sdk") + connection.set("scm:git:git://github.com/tolgee/tolgee-mobile-kotlin-sdk.git") } developers { diff --git a/compiler-plugin/src/main/kotlin/io/tolgee/transformer/AndroidTransformer.kt b/compiler-plugin/src/main/kotlin/io/tolgee/transformer/AndroidTransformer.kt index c41845b0..36632e81 100644 --- a/compiler-plugin/src/main/kotlin/io/tolgee/transformer/AndroidTransformer.kt +++ b/compiler-plugin/src/main/kotlin/io/tolgee/transformer/AndroidTransformer.kt @@ -9,6 +9,8 @@ import org.jetbrains.kotlin.ir.builders.irCall import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.expressions.IrCall import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI import org.jetbrains.kotlin.ir.types.isInt import org.jetbrains.kotlin.ir.util.callableId @@ -19,7 +21,7 @@ import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name /** - * Transforms IR calls to `Context.getString`, replacing them with `getStringInstant` + * Transforms IR calls to `Context.getString`, replacing them with `getStringT` * from `io.tolgee.common` where applicable. * * This transformation ensures that calls to `getString` are intercepted and redirected @@ -72,34 +74,34 @@ internal class AndroidTransformer( ) /** - * Retrieves references to the `getStringInstant` functions from the `io.tolgee.common` package. + * Retrieves references to the `getStringT` functions from the `io.tolgee.common` package. * These functions are intended as replacements for `Context.getString`. */ private val tolgeeGetStringFunctions = pluginContext.referenceFunctions( CallableId( packageName = FqName("io.tolgee.common"), - callableName = Name.identifier("getStringInstant") + callableName = Name.identifier("getStringT") ) ) private val tolgeePluralStringFunctions = pluginContext.referenceFunctions( CallableId( packageName = FqName("io.tolgee.common"), - callableName = Name.identifier("getQuantityStringInstant") + callableName = Name.identifier("getQuantityStringT") ) ) /** - * Visits an `IrCall` expression and replaces calls to `Context.getString` with `getStringInstant` + * Visits an `IrCall` expression and replaces calls to `Context.getString` with `getStringT` * from `io.tolgee.common`, if the plugin configuration enables this transformation. * * The method checks whether the function being called belongs to a subclass of `Context`. * If so, and if the function overrides `getString`, it attempts to replace it with an appropriate - * `getStringInstant` function, ensuring that the first parameter is an integer (`@StringRes resId`) + * `getStringT` function, ensuring that the first parameter is an integer (`@StringRes resId`) * and that the number of parameters matches. * * @param expression The `IrCall` expression being visited. - * @return The transformed `IrExpression`, replacing `getString` with `getStringInstant` where applicable. + * @return The transformed `IrExpression`, replacing `getString` with `getStringT` where applicable. */ @OptIn(UnsafeDuringIrConstructionAPI::class) override fun visitCall(expression: IrCall): IrExpression { @@ -120,67 +122,62 @@ internal class AndroidTransformer( @OptIn(UnsafeDuringIrConstructionAPI::class) private fun visitContext(expression: IrCall): IrExpression? { - val function = expression.symbol.owner - val context = contextClass ?: return null - - val contextReceiver = if (function.dispatchReceiverParameter?.type?.isSubtypeOfClass(context) == true) { - expression.dispatchReceiver - } else if (function.extensionReceiverParameter?.type?.isSubtypeOfClass(context) == true) { - expression.extensionReceiver - } else { - null + return expression.visit(contextClass, contextGetStringCallableId, tolgeeGetStringFunctions) + } + + @OptIn(UnsafeDuringIrConstructionAPI::class) + private fun visitResources(expression: IrCall): IrExpression? { + return expression.visit(resourcesClass, resourcesPluralStringCallableId, tolgeePluralStringFunctions) + } + + @OptIn(UnsafeDuringIrConstructionAPI::class) + private fun IrCall.visit( + clazz: IrClassSymbol?, + replaceFunction: CallableId, + replacementFunctions: Collection + ): IrExpression? { + val classRef = clazz ?: return null + val receiver = getReceiver(classRef) ?: return null + + val function = symbol.owner + if (!function.isOverrideOf(replaceFunction)) { + return null } - if (contextReceiver != null) { - if (function.isOverrideOf(contextGetStringCallableId)) { - val tolgeeMethod = tolgeeGetStringFunctions.firstOrNull { - val resIdFirst = it.owner.valueParameters.firstOrNull()?.type?.isInt() == true - resIdFirst && it.owner.valueParameters.size == function.valueParameters.size - } ?: return null + val tolgeeMethod = replacementFunctions.findReplacementFor(function) ?: return null + + return function.symbol.replace(tolgeeMethod, receiver, valueArguments) + } - return DeclarationIrBuilder(pluginContext, function.symbol).irCall(tolgeeMethod).apply { - extensionReceiver = contextReceiver + private fun IrSimpleFunctionSymbol.replace(replacement: IrSimpleFunctionSymbol, receiver: IrExpression, args: List): IrExpression { + return DeclarationIrBuilder(pluginContext, this).irCall(replacement).apply { + extensionReceiver = receiver - expression.valueArguments.forEachIndexed { index, arg -> - putValueArgument(index, arg) - } - } + arguments[0] = receiver + args.forEachIndexed { index, arg -> + arguments[index+1] = arg } } - - return null } @OptIn(UnsafeDuringIrConstructionAPI::class) - private fun visitResources(expression: IrCall): IrExpression? { - val function = expression.symbol.owner - val resources = resourcesClass ?: return null - - val resourcesReceiver = if (function.dispatchReceiverParameter?.type?.isSubtypeOfClass(resources) == true) { - expression.dispatchReceiver - } else if (function.extensionReceiverParameter?.type?.isSubtypeOfClass(resources) == true) { - expression.extensionReceiver - } else { - null + private fun Collection.findReplacementFor(function: IrSimpleFunction): IrSimpleFunctionSymbol? { + return firstOrNull { + val resIdFirst = it.owner.valueParameters.firstOrNull()?.type?.isInt() == true + resIdFirst && it.owner.valueParameters.size == function.valueParameters.size } + } - if (resourcesReceiver != null) { - if (function.isOverrideOf(resourcesPluralStringCallableId)) { - val tolgeeMethod = tolgeePluralStringFunctions.firstOrNull { - val resIdFirst = it.owner.valueParameters.firstOrNull()?.type?.isInt() == true - resIdFirst && it.owner.valueParameters.size == function.valueParameters.size - } ?: return null - - return DeclarationIrBuilder(pluginContext, function.symbol).irCall(tolgeeMethod).apply { - extensionReceiver = resourcesReceiver + @OptIn(UnsafeDuringIrConstructionAPI::class) + private fun IrCall.getReceiver(clazz: IrClassSymbol): IrExpression? { + val function = symbol.owner - expression.valueArguments.forEachIndexed { index, arg -> - putValueArgument(index, arg) - } - } - } + if (function.dispatchReceiverParameter?.type?.isSubtypeOfClass(clazz) == true) { + return dispatchReceiver + } + if (function.extensionReceiverParameter?.type?.isSubtypeOfClass(clazz) == true) { + return extensionReceiver } - return null } diff --git a/compiler-plugin/src/main/kotlin/io/tolgee/transformer/ComposeTransformer.kt b/compiler-plugin/src/main/kotlin/io/tolgee/transformer/ComposeTransformer.kt index 5a338846..0ea5dc6b 100644 --- a/compiler-plugin/src/main/kotlin/io/tolgee/transformer/ComposeTransformer.kt +++ b/compiler-plugin/src/main/kotlin/io/tolgee/transformer/ComposeTransformer.kt @@ -6,8 +6,11 @@ import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.ir.backend.js.utils.valueArguments import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.expressions.IrCall import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI import org.jetbrains.kotlin.ir.types.IrType import org.jetbrains.kotlin.ir.types.classOrNull import org.jetbrains.kotlin.ir.types.isInt @@ -69,101 +72,68 @@ internal class ComposeTransformer( override fun visitCall(expression: IrCall): IrExpression { if (config.compose.stringResourceReplacement) { - val result = visitAndroidString(expression) ?: visitMultiplatformString(expression) - result?.let { + visitString(expression)?.let { return it } } if (config.compose.pluralStringReplacement) { - val result = visitAndroidPluralString(expression) ?: visitMultiplatformPluralString(expression) - result?.let { + visitPluralString(expression)?.let { return it } } return super.visitCall(expression) } - private fun visitAndroidString(expression: IrCall): IrExpression? { + @OptIn(UnsafeDuringIrConstructionAPI::class) + private fun visitString(expression: IrCall): IrExpression? { val function = expression.symbol.owner - - if (function.callableId == androidStringResourceCallableId) { - val argumentCount = function.valueParameters.size - val replacementMethod = tolgeeStringResourceFunctions.firstOrNull { tolgeeFunction -> - val tolgeeParams = tolgeeFunction.owner.valueParameters - val tolgeeFirstIsInt = tolgeeParams.firstOrNull()?.type?.isInt() == true - argumentCount == tolgeeParams.size && tolgeeFirstIsInt - } ?: return null - - return DeclarationIrBuilder(pluginContext, function.symbol).irCall(replacementMethod).apply { - expression.valueArguments.forEachIndexed { index, arg -> - putValueArgument(index, arg) - } - } + if (function.callableId !in arrayOf(multiplatformStringResourceCallableId, androidStringResourceCallableId)) { + return null } - return null - } - private fun visitMultiplatformString(expression: IrCall): IrExpression? { - val function = expression.symbol.owner - - if (function.callableId == multiplatformStringResourceCallableId) { - val argumentCount = function.valueParameters.size - val replacementMethod = tolgeeStringResourceFunctions.firstOrNull { tolgeeFunction -> - val tolgeeParams = tolgeeFunction.owner.valueParameters - val tolgeeFirstIsStringRes = tolgeeParams.firstOrNull()?.type?.isStringResourceType() == true - argumentCount == tolgeeParams.size && tolgeeFirstIsStringRes - } ?: return null - - return DeclarationIrBuilder(pluginContext, function.symbol).irCall(replacementMethod).apply { - expression.valueArguments.forEachIndexed { index, arg -> - putValueArgument(index, arg) - } - } - } - return null + val replacement = tolgeeStringResourceFunctions.findReplacementFor(function) ?: return null + return function.symbol.replace(replacement, expression.valueArguments) } - private fun visitAndroidPluralString(expression: IrCall): IrExpression? { + @OptIn(UnsafeDuringIrConstructionAPI::class) + private fun visitPluralString(expression: IrCall): IrExpression? { val function = expression.symbol.owner - - if (function.callableId == androidPluralStringResourceCallableId) { - val argumentCount = function.valueParameters.size - val replacementMethod = tolgeePluralStringResourceFunctions.firstOrNull { tolgeeFunction -> - val tolgeeParams = tolgeeFunction.owner.valueParameters - val tolgeeFirstIsInt = tolgeeParams.firstOrNull()?.type?.isInt() == true - argumentCount == tolgeeParams.size && tolgeeFirstIsInt - } ?: return null - - return DeclarationIrBuilder(pluginContext, function.symbol).irCall(replacementMethod).apply { - expression.valueArguments.forEachIndexed { index, arg -> - putValueArgument(index, arg) - } - } + if (function.callableId !in arrayOf(multiplatformPluralStringResourceCallableId, androidPluralStringResourceCallableId)) { + return null } - return null + val replacement = tolgeePluralStringResourceFunctions.findReplacementFor(function) ?: return null + return function.symbol.replace(replacement, expression.valueArguments) } - private fun visitMultiplatformPluralString(expression: IrCall): IrExpression? { - val function = expression.symbol.owner - - if (function.callableId == multiplatformPluralStringResourceCallableId) { - val argumentCount = function.valueParameters.size - val replacementMethod = tolgeePluralStringResourceFunctions.firstOrNull { tolgeeFunction -> - val tolgeeParams = tolgeeFunction.owner.valueParameters - val tolgeeFirstIsPluralStringRes = tolgeeParams.firstOrNull()?.type?.isPluralStringResourceType() == true - argumentCount == tolgeeParams.size && tolgeeFirstIsPluralStringRes - } ?: return null - - return DeclarationIrBuilder(pluginContext, function.symbol).irCall(replacementMethod).apply { - expression.valueArguments.forEachIndexed { index, arg -> - putValueArgument(index, arg) - } + private fun IrSimpleFunctionSymbol.replace(replacement: IrSimpleFunctionSymbol, args: List): IrExpression { + return DeclarationIrBuilder(pluginContext, this).irCall(replacement).apply { + args.forEachIndexed { index, arg -> + arguments[index] = arg } } + } - return null + @OptIn(UnsafeDuringIrConstructionAPI::class) + private fun Collection.findReplacementFor(function: IrSimpleFunction): IrSimpleFunctionSymbol? { + val argumentCount = function.valueParameters.size + val funParams = function.valueParameters + val funFirstIsPluralStringRes = funParams.firstOrNull()?.type?.isPluralStringResourceType() == true + val funFirstIsStringRes = funParams.firstOrNull()?.type?.isStringResourceType() == true + val funFirstIsInt = funParams.firstOrNull()?.type?.isInt() == true + + return firstOrNull { tolgeeFunction -> + val tolgeeParams = tolgeeFunction.owner.valueParameters + val tolgeeFirstIsPluralStringRes = tolgeeParams.firstOrNull()?.type?.isPluralStringResourceType() == true + val tolgeeFirstIsStringRes = tolgeeParams.firstOrNull()?.type?.isStringResourceType() == true + val tolgeeFirstIsInt = tolgeeParams.firstOrNull()?.type?.isInt() == true + + argumentCount == tolgeeParams.size && + tolgeeFirstIsPluralStringRes == funFirstIsPluralStringRes && + tolgeeFirstIsStringRes == funFirstIsStringRes && + tolgeeFirstIsInt == funFirstIsInt + } } private fun IrType.isStringResourceType(): Boolean { @@ -173,5 +143,4 @@ internal class ComposeTransformer( private fun IrType.isPluralStringResourceType(): Boolean { return classOrNull?.owner?.classId == multiplatformPluralStringResourceClassId } - -} \ No newline at end of file +} diff --git a/compose/README.md b/compose/README.md new file mode 100644 index 00000000..5096244c --- /dev/null +++ b/compose/README.md @@ -0,0 +1,218 @@ +# Tolgee Compose Module + +This module provides seamless integration between Tolgee and Compose (both Jetpack Compose for Android and Compose Multiplatform). +It allows you to use Tolgee translations directly in your Compose UI with minimal effort. + +## Features + +- **Compose Integration**: Use Tolgee translations in your Compose UI with familiar APIs +- **Android Resource Support**: Seamless integration with Android string resources +- **Compose Multiplatform Support**: Works with both Jetpack Compose and Compose Multiplatform +- **Parameter Support**: Pass parameters to your translations +- **Plural Support**: Handles plural forms +- **Reactive Updates**: UI automatically updates when translations or locale change + +## Setup + +Using Version Catalog is highly recommended to keep your versions aligned. + +```toml +# gradle/libs.versions.toml +[libraries] +tolgee = { group = "io.tolgee.mobile-kotlin-sdk", name = "compose", version.ref = "tolgee" } +``` + +```kotlin +// build.gradle.kts +dependencies { + implementation(libs.tolgee) +} +``` + +### Android specific steps + +Create a network security config file `network_security.xml` in your `res/xml` folder: + +```xml + + + + tolgee.io + tolg.ee + + +``` + +Add network security config to your `AndroidManifest.xml`: + +```xml + + +``` + +## Usage + +### Initialization + +Initialize Tolgee in your Application class (Android) or at the entry point of your application (Multiplatform): + +#### Android (Jetpack Compose) + +```kotlin +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + + Tolgee.init { + contentDelivery { + url = "https://cdn.tolg.ee/your-cdn-url-prefix" + storage = TolgeeStorageProviderAndroid(this@MyApplication, BuildConfig.VERSION_CODE) + } + } + } +} +``` + +#### Multiplatform + +```kotlin +fun initTolgee() { + Tolgee.init { + contentDelivery { + url = "https://cdn.tolg.ee/your-cdn-url-prefix" + // Create a custom storage provider for caching the latest translations from CDN if needed + } + } +} +``` + +### Basic Usage + +#### Jetpack Compose (Android) + +```kotlin +@Composable +fun SimpleText() { + // Use the stringResource extension function + Text(text = stringResource(R.string.welcome_message)) +} + +@Composable +fun TextWithParameters(name: String) { + // Pass parameters to your translations + Text(text = stringResource(R.string.welcome_user, name)) +} + +@Composable +fun PluralText(count: Int) { + // Handle plural forms + Text(text = pluralStringResource(R.plurals.item_count, count, count)) +} +``` + +#### Compose Multiplatform + +```kotlin +@Composable +fun SimpleText() { + // Use the stringResource extension function with Res + Text(text = stringResource(Res.string.welcome_message)) +} + +@Composable +fun TextWithParameters(name: String) { + // Pass parameters to your translations + Text(text = stringResource(Res.string.welcome_user, name)) +} + +@Composable +fun PluralText(count: Int) { + // Handle plural forms + Text(text = pluralStringResource(Res.plurals.item_count, count, count)) +} +``` + +### Advanced Usage + +#### Explicit Tolgee Instance + +If you need to use a specific Tolgee instance (not the singleton), you can pass it explicitly: + +```kotlin +@Composable +fun ExplicitInstance() { + val tolgee = remember { /* your custom Tolgee instance */ } + + Text(text = stringResource(tolgee, Res.string.welcome_message)) +} +``` + +#### Locale Switching + +You can create a locale switcher component: + +```kotlin +@Composable +fun LocaleSwitcher() { + val tolgee = Tolgee.instance + val currentLocale = tolgee.changeFlow.mapLatest { + tolgee.getLocale() + }.collectAsState(initial = tolgee.getLocale()) + + Row { + Text(text = stringResource(tolgee, R.string.selected_locale, currentLocale.language)) + Button(onClick = { tolgee.setLocale("en") }) { + Text("English") + } + Button(onClick = { tolgee.setLocale("fr") }) { + Text("Français") + } + Button(onClick = { tolgee.setLocale("cs") }) { + Text("Čeština") + } + } +} +``` + +#### Observing Locale Changes + +```kotlin +@Composable +fun LocaleAwareComponent() { + val tolgee = Tolgee.instance + + // This will cause recomposition when the locale changes + val currentLocale = tolgee.changeFlow.mapLatest { + tolgee.getLocale() + }.collectAsState(initial = tolgee.getLocale()) + + // Your UI that depends on the current locale + Text(text = currentLocale.language) +} +``` + +## Troubleshooting + +### Translations Not Updating + +- Ensure you're using the `stringResource` and `pluralStringResource` functions from the Tolgee package +- Check that your Tolgee instance is properly initialized +- Verify that the translations are loaded correctly + +### Android Resource Integration Issues + +- Check that your Android resources are properly structured - Tolgee is using `resources.getResourceEntryName` to find key for the resource +- Ensure that the Tolgee instance has access to the Android context + +### Compose Multiplatform Issues + +- Verify that your resource files are correctly set up and all keys present in resources are present in a Tolgee platform too +- Ensure that you're using the correct resource references + +## Example Projects + +For complete examples of how to use the Tolgee Compose module, check out the demo projects: + +- [Example Jetpack](../demo/examplejetpack) - Jetpack Compose example +- [Multiplatform Compose](../demo/multiplatform-compose) - Compose Multiplatform example \ No newline at end of file diff --git a/compose/api/android/compose.api b/compose/api/android/compose.api index 3abea2a4..e3517c5b 100644 --- a/compose/api/android/compose.api +++ b/compose/api/android/compose.api @@ -1,6 +1,12 @@ public final class io/tolgee/ComposeTolgeeKt { public static final fun locale (Lio/tolgee/Tolgee$Config$Builder;Landroidx/compose/ui/text/intl/Locale;)Lio/tolgee/Tolgee$Config$Builder; + public static final fun pluralStringResource (Lio/tolgee/Tolgee;Lorg/jetbrains/compose/resources/PluralStringResource;ILandroidx/compose/runtime/Composer;I)Ljava/lang/String; + public static final fun pluralStringResource (Lio/tolgee/Tolgee;Lorg/jetbrains/compose/resources/PluralStringResource;I[Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/String; + public static final fun pluralStringResource (Lorg/jetbrains/compose/resources/PluralStringResource;ILandroidx/compose/runtime/Composer;I)Ljava/lang/String; + public static final fun pluralStringResource (Lorg/jetbrains/compose/resources/PluralStringResource;I[Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/String; public static final fun setLocale (Lio/tolgee/Tolgee;Landroidx/compose/ui/text/intl/Locale;)Ljava/util/Locale; + public static final fun stringArrayResource (Lio/tolgee/Tolgee;Lorg/jetbrains/compose/resources/StringArrayResource;Landroidx/compose/runtime/Composer;I)Ljava/util/List; + public static final fun stringArrayResource (Lorg/jetbrains/compose/resources/StringArrayResource;Landroidx/compose/runtime/Composer;I)Ljava/util/List; public static final fun stringResource (Lio/tolgee/Tolgee;Lorg/jetbrains/compose/resources/StringResource;Landroidx/compose/runtime/Composer;I)Ljava/lang/String; public static final fun stringResource (Lio/tolgee/Tolgee;Lorg/jetbrains/compose/resources/StringResource;[Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/String; public static final fun stringResource (Lorg/jetbrains/compose/resources/StringResource;Landroidx/compose/runtime/Composer;I)Ljava/lang/String; @@ -8,6 +14,12 @@ public final class io/tolgee/ComposeTolgeeKt { } public final class io/tolgee/ComposeTolgee_androidKt { + public static final fun pluralStringResource (IILandroidx/compose/runtime/Composer;I)Ljava/lang/String; + public static final fun pluralStringResource (II[Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/String; + public static final fun pluralStringResource (Lio/tolgee/Tolgee;IILandroidx/compose/runtime/Composer;I)Ljava/lang/String; + public static final fun pluralStringResource (Lio/tolgee/Tolgee;II[Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/String; + public static final fun stringArrayResource (ILandroidx/compose/runtime/Composer;I)[Ljava/lang/String; + public static final fun stringArrayResource (Lio/tolgee/Tolgee;ILandroidx/compose/runtime/Composer;I)[Ljava/lang/String; public static final fun stringResource (ILandroidx/compose/runtime/Composer;I)Ljava/lang/String; public static final fun stringResource (I[Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/String; public static final fun stringResource (Lio/tolgee/Tolgee;ILandroidx/compose/runtime/Composer;I)Ljava/lang/String; diff --git a/compose/api/jvm/compose.api b/compose/api/jvm/compose.api index 75fcee76..3f6f4ae6 100644 --- a/compose/api/jvm/compose.api +++ b/compose/api/jvm/compose.api @@ -1,6 +1,12 @@ public final class io/tolgee/ComposeTolgeeKt { public static final fun locale (Lio/tolgee/Tolgee$Config$Builder;Landroidx/compose/ui/text/intl/Locale;)Lio/tolgee/Tolgee$Config$Builder; + public static final fun pluralStringResource (Lio/tolgee/Tolgee;Lorg/jetbrains/compose/resources/PluralStringResource;ILandroidx/compose/runtime/Composer;I)Ljava/lang/String; + public static final fun pluralStringResource (Lio/tolgee/Tolgee;Lorg/jetbrains/compose/resources/PluralStringResource;I[Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/String; + public static final fun pluralStringResource (Lorg/jetbrains/compose/resources/PluralStringResource;ILandroidx/compose/runtime/Composer;I)Ljava/lang/String; + public static final fun pluralStringResource (Lorg/jetbrains/compose/resources/PluralStringResource;I[Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/String; public static final fun setLocale (Lio/tolgee/Tolgee;Landroidx/compose/ui/text/intl/Locale;)Ljava/util/Locale; + public static final fun stringArrayResource (Lio/tolgee/Tolgee;Lorg/jetbrains/compose/resources/StringArrayResource;Landroidx/compose/runtime/Composer;I)Ljava/util/List; + public static final fun stringArrayResource (Lorg/jetbrains/compose/resources/StringArrayResource;Landroidx/compose/runtime/Composer;I)Ljava/util/List; public static final fun stringResource (Lio/tolgee/Tolgee;Lorg/jetbrains/compose/resources/StringResource;Landroidx/compose/runtime/Composer;I)Ljava/lang/String; public static final fun stringResource (Lio/tolgee/Tolgee;Lorg/jetbrains/compose/resources/StringResource;[Ljava/lang/Object;Landroidx/compose/runtime/Composer;I)Ljava/lang/String; public static final fun stringResource (Lorg/jetbrains/compose/resources/StringResource;Landroidx/compose/runtime/Composer;I)Ljava/lang/String; diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts index 581bd0d1..44e65310 100644 --- a/compose/build.gradle.kts +++ b/compose/build.gradle.kts @@ -14,7 +14,7 @@ plugins { signing } -val libGroup = "io.tolgee" +val libGroup = "io.tolgee.mobile-kotlin-sdk" val libName = "compose" group = libGroup @@ -25,7 +25,7 @@ dokka { dokkaSourceSets.configureEach { sourceLink { localDirectory.set(file("src")) - remoteUrl("https://github.com/tolgee/compose-tolgee/tree/master/compose/src") + remoteUrl("https://github.com/tolgee/tolgee-mobile-kotlin-sdk/tree/master/compose/src") } } } @@ -75,7 +75,7 @@ kotlin { android { compileSdk = 35 - namespace = "io.tolgee.compose" + namespace = "io.tolgee.mobilekotlinsdk.compose" defaultConfig { minSdk = 21 @@ -101,7 +101,7 @@ mavenPublishing { name.set(libName) description.set("Compose Multiplatform localization wrapper for Tolgee") - url.set("https://github.com/tolgee/compose-tolgee") + url.set("https://github.com/tolgee/tolgee-mobile-kotlin-sdk") licenses { license { @@ -111,8 +111,8 @@ mavenPublishing { } scm { - url.set("https://github.com/tolgee/compose-tolgee") - connection.set("scm:git:git://github.com/tolgee/compose-tolgee.git") + url.set("https://github.com/tolgee/tolgee-mobile-kotlin-sdk") + connection.set("scm:git:git://github.com/tolgee/tolgee-mobile-kotlin-sdk.git") } developers { diff --git a/compose/src/androidMain/kotlin/io/tolgee/ComposeTolgee.android.kt b/compose/src/androidMain/kotlin/io/tolgee/ComposeTolgee.android.kt index 0b3255f3..b11b2ae1 100644 --- a/compose/src/androidMain/kotlin/io/tolgee/ComposeTolgee.android.kt +++ b/compose/src/androidMain/kotlin/io/tolgee/ComposeTolgee.android.kt @@ -25,9 +25,9 @@ fun stringResource(tolgee: Tolgee, @StringRes id: Int): String { TolgeeAndroid.getKeyFromResources(context, id) } - val translationFlow = (tolgee as? TolgeeAndroid)?.translation(context, id) + val translationFlow = (tolgee as? TolgeeAndroid)?.tFlow(context, id) ?: (key ?: TolgeeAndroid.getKeyFromResources(context, id))?.let { - tolgee.translation(key = it) + tolgee.tFlow(key = it) } ?: flowOf(androidx.compose.ui.res.stringResource(id)) return translationFlow.collectAsState( @@ -47,7 +47,7 @@ fun stringResource(tolgee: Tolgee, @StringRes id: Int): String { */ @Composable fun stringResource(@StringRes id: Int): String { - val instance = Tolgee.instance ?: return androidx.compose.ui.res.stringResource(id) + val instance = Tolgee.instanceOrNull ?: return androidx.compose.ui.res.stringResource(id) return stringResource(instance, id) } @@ -68,9 +68,9 @@ fun stringResource(tolgee: Tolgee, @StringRes id: Int, vararg formatArgs: Any): TolgeeAndroid.getKeyFromResources(context, id) } - val translationFlow = (tolgee as? TolgeeAndroid)?.translation(context, id, *formatArgs) + val translationFlow = (tolgee as? TolgeeAndroid)?.tFlow(context, id, *formatArgs) ?: (key ?: TolgeeAndroid.getKeyFromResources(context, id))?.let { - tolgee.translation(key = it, parameters = TolgeeMessageParams.Indexed(*formatArgs)) + tolgee.tFlow(key = it, parameters = TolgeeMessageParams.Indexed(*formatArgs)) } ?: flowOf(androidx.compose.ui.res.stringResource(id, *formatArgs)) return translationFlow.collectAsState( @@ -88,7 +88,7 @@ fun stringResource(tolgee: Tolgee, @StringRes id: Int, vararg formatArgs: Any): */ @Composable fun stringResource(@StringRes id: Int, vararg formatArgs: Any): String { - val instance = Tolgee.instance ?: return androidx.compose.ui.res.stringResource(id, *formatArgs) + val instance = Tolgee.instanceOrNull ?: return androidx.compose.ui.res.stringResource(id, *formatArgs) return stringResource(instance, id, *formatArgs) } @@ -100,19 +100,19 @@ fun pluralStringResource(tolgee: Tolgee, @PluralsRes id: Int, quantity: Int): St TolgeeAndroid.getKeyFromResources(context, id) } - val translationFlow = (tolgee as? TolgeeAndroid)?.pluralTranslation(context.resources, id, quantity) + val translationFlow = (tolgee as? TolgeeAndroid)?.tPluralFlow(context.resources, id, quantity) ?: (key ?: TolgeeAndroid.getKeyFromResources(context, id))?.let { - tolgee.translation(key = it, parameters = TolgeeMessageParams.Indexed(quantity)) - } ?: flowOf(androidx.compose.ui.res.pluralStringResource(id, quantity)) + tolgee.tFlow(key = it, parameters = TolgeeMessageParams.Indexed(quantity)) + } ?: flowOf(androidx.compose.ui.res.pluralStringResource(id, quantity, quantity)) return translationFlow.collectAsState( - initial = androidx.compose.ui.res.pluralStringResource(id, quantity) + initial = androidx.compose.ui.res.pluralStringResource(id, quantity, quantity) ).value } @Composable fun pluralStringResource(@PluralsRes id: Int, quantity: Int): String { - val instance = Tolgee.instance ?: return androidx.compose.ui.res.pluralStringResource(id, quantity) + val instance = Tolgee.instanceOrNull ?: return androidx.compose.ui.res.pluralStringResource(id, quantity, quantity) return pluralStringResource(instance, id, quantity) } @@ -124,16 +124,23 @@ fun pluralStringResource(tolgee: Tolgee, @PluralsRes id: Int, quantity: Int, var TolgeeAndroid.getKeyFromResources(context, id) } - val translationFlow = (tolgee as? TolgeeAndroid)?.pluralTranslation(context.resources, id, quantity, *formatArgs) + val translationFlow = (tolgee as? TolgeeAndroid)?.tPluralFlow(context.resources, id, quantity, *formatArgs) ?: (key ?: TolgeeAndroid.getKeyFromResources(context, id))?.let { - tolgee.translation(key = it, parameters = TolgeeMessageParams.Indexed(quantity, *formatArgs)) - } ?: flowOf(androidx.compose.ui.res.pluralStringResource(id, quantity, *formatArgs)) + tolgee.tFlow(key = it, parameters = TolgeeMessageParams.Indexed(quantity, *formatArgs)) + } ?: flowOf(androidx.compose.ui.res.pluralStringResource(id, quantity, quantity, *formatArgs)) return translationFlow.collectAsState( - initial = androidx.compose.ui.res.pluralStringResource(id, quantity, *formatArgs) + initial = androidx.compose.ui.res.pluralStringResource(id, quantity, quantity, *formatArgs) ).value } +@Composable +fun pluralStringResource(@PluralsRes id: Int, quantity: Int, vararg formatArgs: Any): String { + val instance = Tolgee.instanceOrNull ?: return androidx.compose.ui.res.pluralStringResource(id, quantity, quantity, *formatArgs) + + return pluralStringResource(instance, id, quantity, *formatArgs) +} + @Composable fun stringArrayResource(tolgee: Tolgee, @ArrayRes id: Int): Array { val context = LocalContext.current @@ -141,9 +148,9 @@ fun stringArrayResource(tolgee: Tolgee, @ArrayRes id: Int): Array { TolgeeAndroid.getKeyFromResources(context, id) } - val translationFlow = (tolgee as? TolgeeAndroid)?.stringArrayTranslation(context.resources, id) + val translationFlow = (tolgee as? TolgeeAndroid)?.tArrayFlow(context.resources, id) ?: (key ?: TolgeeAndroid.getKeyFromResources(context, id))?.let { - tolgee.stringArrayTranslation(key = it) + tolgee.tArrayFlow(key = it) } ?: flowOf(androidx.compose.ui.res.stringArrayResource(id).toList()) return translationFlow.collectAsState( @@ -153,6 +160,6 @@ fun stringArrayResource(tolgee: Tolgee, @ArrayRes id: Int): Array { @Composable fun stringArrayResource(@ArrayRes id: Int): Array { - val instance = Tolgee.instance ?: return androidx.compose.ui.res.stringArrayResource(id) + val instance = Tolgee.instanceOrNull ?: return androidx.compose.ui.res.stringArrayResource(id) return stringArrayResource(instance, id) } \ No newline at end of file diff --git a/compose/src/appleMain/kotlin/io/tolgee/ComposeTolgee.apple.kt b/compose/src/appleMain/kotlin/io/tolgee/ComposeTolgee.apple.kt index 8deebdc1..678a52c6 100644 --- a/compose/src/appleMain/kotlin/io/tolgee/ComposeTolgee.apple.kt +++ b/compose/src/appleMain/kotlin/io/tolgee/ComposeTolgee.apple.kt @@ -15,7 +15,7 @@ import io.tolgee.model.TolgeeMessageParams */ @Composable fun stringResource(tolgee: Tolgee, key: String, default: String?, table: String? = null): String { - val translationFlow = (tolgee as? TolgeeApple)?.translation(key, default, table) ?: tolgee.translation(key) + val translationFlow = (tolgee as? TolgeeApple)?.tFlow(key, default, table) ?: tolgee.tFlow(key) val res = tolgee.getLocale().getLanguage().ifBlank { null } return translationFlow.collectAsState( @@ -36,7 +36,7 @@ fun stringResource(tolgee: Tolgee, key: String, default: String?, table: String? */ @Composable fun stringResource(key: String, default: String?, table: String? = null): String { - val instance = Tolgee.instance ?: return run { + val instance = Tolgee.instanceOrNull ?: return run { val res = Tolgee.systemLocale.getLanguage().ifBlank { null } TolgeeApple.getLocalizedStringFromBundle(res, key, default, table) ?: default?.ifBlank { null } ?: "" @@ -59,8 +59,8 @@ fun stringResource(key: String, default: String?, table: String? = null): String */ @Composable fun stringResource(tolgee: Tolgee, key: String, default: String?, table: String? = null, vararg args: Any): String { - val translationFlow = (tolgee as? TolgeeApple)?.translation(key, default, table, *args) - ?: tolgee.translation(key, TolgeeMessageParams.Indexed(*args)) + val translationFlow = (tolgee as? TolgeeApple)?.tFlow(key, default, table, *args) + ?: tolgee.tFlow(key, TolgeeMessageParams.Indexed(*args)) val res = tolgee.getLocale().getLanguage().ifBlank { null } return translationFlow.collectAsState( @@ -80,7 +80,7 @@ fun stringResource(tolgee: Tolgee, key: String, default: String?, table: String? */ @Composable fun stringResource(key: String, default: String?, table: String? = null, vararg args: Any): String { - val instance = Tolgee.instance ?: return run { + val instance = Tolgee.instanceOrNull ?: return run { val res = Tolgee.systemLocale.getLanguage().ifBlank { null } TolgeeApple.getLocalizedStringFromBundleFormatted(res, key, default, table, *args) ?: "" diff --git a/compose/src/commonMain/kotlin/io/tolgee/ComposeTolgee.kt b/compose/src/commonMain/kotlin/io/tolgee/ComposeTolgee.kt index bd716448..554b8835 100644 --- a/compose/src/commonMain/kotlin/io/tolgee/ComposeTolgee.kt +++ b/compose/src/commonMain/kotlin/io/tolgee/ComposeTolgee.kt @@ -3,7 +3,6 @@ package io.tolgee import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.text.intl.Locale -import io.tolgee.common.mapNotNull import io.tolgee.model.TolgeeMessageParams import org.jetbrains.compose.resources.PluralStringResource import org.jetbrains.compose.resources.StringArrayResource @@ -18,7 +17,7 @@ import org.jetbrains.compose.resources.StringResource */ @Composable fun stringResource(tolgee: Tolgee, resource: StringResource): String { - return tolgee.translation( + return tolgee.tFlow( key = resource.key, parameters = TolgeeMessageParams.None ).collectAsState( @@ -37,7 +36,7 @@ fun stringResource(tolgee: Tolgee, resource: StringResource): String { */ @Composable fun stringResource(resource: StringResource): String { - val tolgee = Tolgee.instance ?: return org.jetbrains.compose.resources.stringResource(resource) + val tolgee = Tolgee.instanceOrNull ?: return org.jetbrains.compose.resources.stringResource(resource) return stringResource(tolgee, resource) } @@ -51,7 +50,7 @@ fun stringResource(resource: StringResource): String { */ @Composable fun stringResource(tolgee: Tolgee, resource: StringResource, vararg formatArgs: Any): String { - return tolgee.translation( + return tolgee.tFlow( key = resource.key, parameters = TolgeeMessageParams.Indexed(*formatArgs) ).collectAsState( @@ -71,13 +70,13 @@ fun stringResource(tolgee: Tolgee, resource: StringResource, vararg formatArgs: */ @Composable fun stringResource(resource: StringResource, vararg formatArgs: Any): String { - val tolgee = Tolgee.instance ?: return org.jetbrains.compose.resources.stringResource(resource, *formatArgs) + val tolgee = Tolgee.instanceOrNull ?: return org.jetbrains.compose.resources.stringResource(resource, *formatArgs) return stringResource(tolgee, resource, *formatArgs) } @Composable fun pluralStringResource(tolgee: Tolgee, resource: PluralStringResource, quantity: Int): String { - return tolgee.translation( + return tolgee.tFlow( key = resource.key, parameters = TolgeeMessageParams.Indexed(quantity) ).collectAsState( @@ -87,29 +86,29 @@ fun pluralStringResource(tolgee: Tolgee, resource: PluralStringResource, quantit @Composable fun pluralStringResource(resource: PluralStringResource, quantity: Int): String { - val tolgee = Tolgee.instance ?: return org.jetbrains.compose.resources.pluralStringResource(resource, quantity) + val tolgee = Tolgee.instanceOrNull ?: return org.jetbrains.compose.resources.pluralStringResource(resource, quantity) return pluralStringResource(tolgee, resource, quantity) } @Composable fun pluralStringResource(tolgee: Tolgee, resource: PluralStringResource, quantity: Int, vararg formatArgs: Any): String { - return tolgee.translation( + return tolgee.tFlow( key = resource.key, parameters = TolgeeMessageParams.Indexed(quantity, *formatArgs) ).collectAsState( - initial = org.jetbrains.compose.resources.pluralStringResource(resource, quantity, *formatArgs) + initial = org.jetbrains.compose.resources.pluralStringResource(resource, quantity, quantity, *formatArgs) ).value } @Composable fun pluralStringResource(resource: PluralStringResource, quantity: Int, vararg formatArgs: Any): String { - val tolgee = Tolgee.instance ?: return org.jetbrains.compose.resources.pluralStringResource(resource, quantity, *formatArgs) + val tolgee = Tolgee.instanceOrNull ?: return org.jetbrains.compose.resources.pluralStringResource(resource, quantity, quantity, *formatArgs) return pluralStringResource(tolgee, resource, quantity, *formatArgs) } @Composable fun stringArrayResource(tolgee: Tolgee, resource: StringArrayResource): List { - return tolgee.stringArrayTranslation( + return tolgee.tArrayFlow( key = resource.key, ).collectAsState( initial = org.jetbrains.compose.resources.stringArrayResource(resource) @@ -118,7 +117,7 @@ fun stringArrayResource(tolgee: Tolgee, resource: StringArrayResource): List { - val tolgee = Tolgee.instance ?: return org.jetbrains.compose.resources.stringArrayResource(resource) + val tolgee = Tolgee.instanceOrNull ?: return org.jetbrains.compose.resources.stringArrayResource(resource) return stringArrayResource(tolgee, resource) } diff --git a/core/README.md b/core/README.md new file mode 100644 index 00000000..8636c7dd --- /dev/null +++ b/core/README.md @@ -0,0 +1,191 @@ +# Tolgee Core Module + +This Kotlin Multiplatform library provides runtime support for Tolgee translations in your app. +With Tolgee, you can update your translations over-the-air without releasing a new app version. + +## Features + +- **Over-the-air updates**: Update your translations without releasing a new app version +- **Multiple format support**: + - Sprintf (Android SDK) formatting + - ICU (Tolgee Native Flat JSON) formatting +- **Kotlin Multiplatform**: Designed with multiplatform support in mind +- **Android integration**: Seamless integration with Android resources +- **Dynamic locale switching**: Change languages at runtime + +## Setup + +Using Version Catalog is highly recommended to keep your versions aligned. + +```toml +# gradle/libs.versions.toml +[libraries] +tolgee = { group = "io.tolgee.mobile-kotlin-sdk", name = "core", version.ref = "tolgee" } +``` + +```kotlin +// build.gradle.kts +dependencies { + implementation(libs.tolgee) +} +``` + +### Android specific steps + +Create a network security config file `network_security.xml` in your `res/xml` folder: + +```xml + + + + tolgee.io + tolg.ee + + +``` + +Add network security config to your `AndroidManifest.xml`: + +```xml + + +``` + +## Usage + +### Initialization + +#### Android + +Initialize Tolgee in your Application class: + +```kotlin +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + + Tolgee.init { + contentDelivery { + url = "https://cdn.tolg.ee/your-cdn-url-prefix" + storage = TolgeeStorageProviderAndroid(this@MyApplication, BuildConfig.VERSION_CODE) + } + } + } +} +``` + +#### Other Platforms + +For non-Android platforms, initialization is similar but without the Android-specific storage provider: + +```kotlin +fun initTolgee() { + Tolgee.init { + contentDelivery { + url = "https://cdn.tolg.ee/your-cdn-url-prefix" + // Create a custom storage provider for caching the latest translations from CDN if needed + } + } +} +``` + +### Basic Usage + +#### Getting Translations + +```kotlin +// Get the Tolgee instance +val tolgee = Tolgee.instance + +// Get a translation (returns null if not loaded yet) +val text: String? = tolgee.t("key") + +// Get a translation with parameters +val textWithParams: String? = tolgee.t("key_with_param", mapOf("param" to "value")) + +// Get a translation as a Flow (updates automatically when translations change) +val textFlow: Flow = tolgee.tFlow("key") +textFlow.collect { text -> + // Use the text (e.g., update UI) +} +``` + +#### Android-Specific Usage + +```kotlin +// Get a translation with fallback to Android resources +val text = tolgee.t(context, R.string.string_key) + +// Get a translation with fallback to Android resources with parameters +val textWithParams = tolgee.t(context, R.string.string_with_params, "param1", "param2") + +// Get a translation with fallback to Android resources as a Flow +val textFlow = tolgee.tFlow(context, R.string.string_key) +``` + +### Locale Management + +```kotlin +// Set locale +tolgee.setLocale("en") + +// Get current locale +val locale = tolgee.getLocale() + +// Listen for changes +tolgee.changeFlow.collect { + // Locale or available translations changed, update UI if needed +} +``` + +### Advanced Configuration + +#### Formatter Configuration + +```kotlin +Tolgee.init { + contentDelivery { + url = "https://cdn.tolg.ee/your-cdn-url-prefix" + // Configure formatters for parsing translations from CDN + format(Tolgee.Formatter.Sprintf) // Android SDK formatting (default) + // format(Tolgee.Formatter.ICU) // Tolgee Native Flat JSON formatting + } +} +``` + +#### Preloading Translations + +```kotlin +// Preload translations for the current locale from Activity +override fun onStart() { + super.onStart() + tolgee.preload(this) +} +``` + +## Troubleshooting + +### Translations Not Loading + +- Ensure you have the correct Content Delivery URL +- Check that your storage provider is properly configured +- Make sure you're calling `Tolgee.init` before accessing translations +- Use `tolgee.preload` or `tolgee.tFlow` before calling `tolgee.t` to ensure translations are loaded + +### Locale Issues + +- Verify that your project supports the locale code you're using +- Check that translations for the selected locale exist in your Tolgee project +- Use `tolgee.changeFlow` to monitor locale changes and update your UI accordingly + +### Android Integration Issues + +- Ensure that the `TolgeeStorageProviderAndroid` is properly initialized with the context and version code which changes with each app update +- Check that your Android resources are properly structured - Tolgee is using `resources.getResourceEntryName` to find key for the resource + +## Example Projects + +For complete examples of how to use the Tolgee Core module, check out the demo projects: + +- [Example Android](../demo/exampleandroid) - Traditional Android Views example \ No newline at end of file diff --git a/core/Tolgee.podspec b/core/Tolgee.podspec index e51cd914..ec25a627 100644 --- a/core/Tolgee.podspec +++ b/core/Tolgee.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'Tolgee' spec.version = '1.0.0-alpha01' - spec.homepage = 'https://github.com/tolgee/compose-tolgee' + spec.homepage = 'https://github.com/tolgee/tolgee-mobile-kotlin-sdk' spec.source = { :http=> ''} spec.authors = '' spec.license = 'Apache License 2.0' diff --git a/core/api/android/core.api b/core/api/android/core.api index 79830e54..8b3d5823 100644 --- a/core/api/android/core.api +++ b/core/api/android/core.api @@ -1,45 +1,43 @@ public class io/tolgee/Tolgee { public static final field Companion Lio/tolgee/Tolgee$Companion; public fun (Lio/tolgee/Tolgee$Config;)V + public final fun addChangeListener (Lio/tolgee/Tolgee$ChangeListener;)V + public final fun getChangeFlow ()Lkotlinx/coroutines/flow/MutableSharedFlow; public fun getConfig ()Lio/tolgee/Tolgee$Config; public static final fun getInstance ()Lio/tolgee/TolgeeAndroid; + public static final fun getInstanceOrNull ()Lio/tolgee/TolgeeAndroid; public fun getLocale ()Ljava/util/Locale; public static final fun getSystemLocale ()Ljava/util/Locale; - public static final fun init (Lio/tolgee/Tolgee$Config;)Lio/tolgee/TolgeeAndroid; - public static final fun init (Lkotlin/jvm/functions/Function1;)Lio/tolgee/TolgeeAndroid; - public static final fun init (ZLio/tolgee/Tolgee$Config;)Lio/tolgee/TolgeeAndroid; - public static final fun init (ZLkotlin/jvm/functions/Function1;)Lio/tolgee/TolgeeAndroid; - public static final fun instanceOrInit (Lio/tolgee/Tolgee$Config;)Lio/tolgee/TolgeeAndroid; - public static final fun instanceOrInit (Lkotlin/jvm/functions/Function1;)Lio/tolgee/TolgeeAndroid; - public static final fun instanceOrInit (ZLio/tolgee/Tolgee$Config;)Lio/tolgee/TolgeeAndroid; - public static final fun instanceOrInit (ZLkotlin/jvm/functions/Function1;)Lio/tolgee/TolgeeAndroid; - public final fun instant (Ljava/lang/String;)Ljava/lang/String; - public fun instant (Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;)Ljava/lang/String; - public static synthetic fun instant$default (Lio/tolgee/Tolgee;Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;ILjava/lang/Object;)Ljava/lang/String; + public static final fun init (Lio/tolgee/Tolgee$Config;)V + public static final fun init (Lkotlin/jvm/functions/Function1;)V + public static final fun new (Lio/tolgee/Tolgee$Config;)Lio/tolgee/TolgeeAndroid; + public static final fun new (Lkotlin/jvm/functions/Function1;)Lio/tolgee/TolgeeAndroid; public fun preload (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun setLocale (Lio/tolgee/model/TolgeeProjectLanguage;)Ljava/util/Locale; + public final fun removeChangeListener (Lio/tolgee/Tolgee$ChangeListener;)Z public fun setLocale (Ljava/lang/String;)Ljava/util/Locale; public fun setLocale (Ljava/util/Locale;)Ljava/util/Locale; - public final fun translation (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; - public fun translation (Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun translation$default (Lio/tolgee/Tolgee;Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public final fun t (Ljava/lang/String;)Ljava/lang/String; + public fun t (Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;)Ljava/lang/String; + public static synthetic fun t$default (Lio/tolgee/Tolgee;Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;ILjava/lang/Object;)Ljava/lang/String; + public fun tArray (Ljava/lang/String;)Ljava/util/List; + public fun tArrayFlow (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; + public final fun tFlow (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; + public fun tFlow (Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun tFlow$default (Lio/tolgee/Tolgee;Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; +} + +public abstract interface class io/tolgee/Tolgee$ChangeListener { + public abstract fun onTranslationsChanged ()V } public final class io/tolgee/Tolgee$Companion { public final fun getInstance ()Lio/tolgee/TolgeeAndroid; + public final fun getInstanceOrNull ()Lio/tolgee/TolgeeAndroid; public final fun getSystemLocale ()Ljava/util/Locale; - public final fun init (Lio/tolgee/Tolgee$Config;)Lio/tolgee/TolgeeAndroid; - public final fun init (Lkotlin/jvm/functions/Function1;)Lio/tolgee/TolgeeAndroid; - public final fun init (ZLio/tolgee/Tolgee$Config;)Lio/tolgee/TolgeeAndroid; - public final fun init (ZLkotlin/jvm/functions/Function1;)Lio/tolgee/TolgeeAndroid; - public static synthetic fun init$default (Lio/tolgee/Tolgee$Companion;ZLio/tolgee/Tolgee$Config;ILjava/lang/Object;)Lio/tolgee/TolgeeAndroid; - public static synthetic fun init$default (Lio/tolgee/Tolgee$Companion;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/tolgee/TolgeeAndroid; - public final fun instanceOrInit (Lio/tolgee/Tolgee$Config;)Lio/tolgee/TolgeeAndroid; - public final fun instanceOrInit (Lkotlin/jvm/functions/Function1;)Lio/tolgee/TolgeeAndroid; - public final fun instanceOrInit (ZLio/tolgee/Tolgee$Config;)Lio/tolgee/TolgeeAndroid; - public final fun instanceOrInit (ZLkotlin/jvm/functions/Function1;)Lio/tolgee/TolgeeAndroid; - public static synthetic fun instanceOrInit$default (Lio/tolgee/Tolgee$Companion;ZLio/tolgee/Tolgee$Config;ILjava/lang/Object;)Lio/tolgee/TolgeeAndroid; - public static synthetic fun instanceOrInit$default (Lio/tolgee/Tolgee$Companion;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/tolgee/TolgeeAndroid; + public final fun init (Lio/tolgee/Tolgee$Config;)V + public final fun init (Lkotlin/jvm/functions/Function1;)V + public final fun new (Lio/tolgee/Tolgee$Config;)Lio/tolgee/TolgeeAndroid; + public final fun new (Lkotlin/jvm/functions/Function1;)Lio/tolgee/TolgeeAndroid; } public final class io/tolgee/Tolgee$Config { @@ -66,7 +64,6 @@ public final class io/tolgee/Tolgee$Config$Builder { public final fun getContentDelivery ()Lio/tolgee/Tolgee$Config$ContentDelivery; public final fun getLocale ()Ljava/util/Locale; public final fun getNetwork ()Lio/tolgee/Tolgee$Config$Network; - public final fun locale (Lio/tolgee/model/TolgeeProjectLanguage;)Lio/tolgee/Tolgee$Config$Builder; public final fun locale (Ljava/lang/String;)Lio/tolgee/Tolgee$Config$Builder; public final fun locale (Ljava/util/Locale;)Lio/tolgee/Tolgee$Config$Builder; public final fun network (Lio/tolgee/Tolgee$Config$Network;)Lio/tolgee/Tolgee$Config$Builder; @@ -83,9 +80,13 @@ public final class io/tolgee/Tolgee$Config$ContentDelivery { public static final field Companion Lio/tolgee/Tolgee$Config$ContentDelivery$Companion; public fun ()V public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lio/tolgee/Tolgee$Formatter; + public final fun component2 ()Lkotlin/jvm/functions/Function1; + public final fun component3 ()Lio/tolgee/storage/TolgeeStorageProvider; + public final fun component4 ()Lio/tolgee/Tolgee$Formatter; public fun equals (Ljava/lang/Object;)Z public final fun getFormatter ()Lio/tolgee/Tolgee$Formatter; + public final fun getPath ()Lkotlin/jvm/functions/Function1; + public final fun getStorage ()Lio/tolgee/storage/TolgeeStorageProvider; public final fun getUrl ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -96,9 +97,15 @@ public final class io/tolgee/Tolgee$Config$ContentDelivery$Builder { public final fun build ()Lio/tolgee/Tolgee$Config$ContentDelivery; public final fun formatter (Lio/tolgee/Tolgee$Formatter;)Lio/tolgee/Tolgee$Config$ContentDelivery$Builder; public final fun getFormatter ()Lio/tolgee/Tolgee$Formatter; + public final fun getPath ()Lkotlin/jvm/functions/Function1; + public final fun getStorage ()Lio/tolgee/storage/TolgeeStorageProvider; public final fun getUrl ()Ljava/lang/String; + public final fun path (Lkotlin/jvm/functions/Function1;)Lio/tolgee/Tolgee$Config$ContentDelivery$Builder; public final fun setFormatter (Lio/tolgee/Tolgee$Formatter;)V + public final fun setPath (Lkotlin/jvm/functions/Function1;)V + public final fun setStorage (Lio/tolgee/storage/TolgeeStorageProvider;)V public final fun setUrl (Ljava/lang/String;)V + public final fun storage (Lio/tolgee/storage/TolgeeStorageProvider;)Lio/tolgee/Tolgee$Config$ContentDelivery$Builder; public final fun url (Ljava/lang/String;)Lio/tolgee/Tolgee$Config$ContentDelivery$Builder; } @@ -155,16 +162,24 @@ public final class io/tolgee/TolgeeAndroid : io/tolgee/Tolgee { public fun equals (Ljava/lang/Object;)Z public fun getConfig ()Lio/tolgee/Tolgee$Config; public fun hashCode ()I - public final fun instant (Landroid/content/Context;I)Ljava/lang/String; - public final fun instant (Landroid/content/Context;I[Ljava/lang/Object;)Ljava/lang/String; public final fun preload (Landroidx/lifecycle/LifecycleOwner;)Lkotlinx/coroutines/Job; + public final fun t (Landroid/content/Context;I)Ljava/lang/String; + public final fun t (Landroid/content/Context;I[Ljava/lang/Object;)Ljava/lang/String; + public final fun tArray (Landroid/content/res/Resources;I)Ljava/util/List; + public final fun tArrayFlow (Landroid/content/res/Resources;I)Lkotlinx/coroutines/flow/Flow; + public final fun tFlow (Landroid/content/Context;I)Lkotlinx/coroutines/flow/Flow; + public final fun tFlow (Landroid/content/Context;I[Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public final fun tPlural (Landroid/content/res/Resources;II)Ljava/lang/String; + public final fun tPlural (Landroid/content/res/Resources;II[Ljava/lang/Object;)Ljava/lang/String; + public final fun tPluralFlow (Landroid/content/res/Resources;II)Lkotlinx/coroutines/flow/Flow; + public final fun tPluralFlow (Landroid/content/res/Resources;II[Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public final fun tStyled (Landroid/content/Context;I)Ljava/lang/CharSequence; public fun toString ()Ljava/lang/String; - public final fun translation (Landroid/content/Context;I)Lkotlinx/coroutines/flow/Flow; - public final fun translation (Landroid/content/Context;I[Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; } public final class io/tolgee/TolgeeAndroid$Companion { - public final fun getKeyFromStringResource (Landroid/content/Context;I)Ljava/lang/String; + public final fun getKeyFromResources (Landroid/content/Context;I)Ljava/lang/String; + public final fun getKeyFromResources (Landroid/content/res/Resources;I)Ljava/lang/String; } public final class io/tolgee/TolgeeContextWrapper : android/content/ContextWrapper { @@ -182,15 +197,17 @@ public final class io/tolgee/TolgeeContextWrapper$Companion { public final fun wrap (Landroid/content/Context;Lio/tolgee/Tolgee;)Landroid/content/ContextWrapper; } -public final class io/tolgee/common/ExtendCoroutinesKt { - public static final fun mapNotNull (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; -} - public final class io/tolgee/common/ExtendTolgee_androidKt { - public static final fun getStringInstant (Landroid/content/Context;I)Ljava/lang/String; - public static final fun getStringInstant (Landroid/content/Context;I[Ljava/lang/Object;)Ljava/lang/String; - public static final fun getStringInstant (Landroid/content/Context;Lio/tolgee/Tolgee;I)Ljava/lang/String; - public static final fun getStringInstant (Landroid/content/Context;Lio/tolgee/Tolgee;I[Ljava/lang/Object;)Ljava/lang/String; + public static final fun getQuantityStringT (Landroid/content/res/Resources;II)Ljava/lang/String; + public static final fun getQuantityStringT (Landroid/content/res/Resources;II[Ljava/lang/Object;)Ljava/lang/String; + public static final fun getQuantityStringT (Landroid/content/res/Resources;Lio/tolgee/Tolgee;II)Ljava/lang/String; + public static final fun getQuantityStringT (Landroid/content/res/Resources;Lio/tolgee/Tolgee;II[Ljava/lang/Object;)Ljava/lang/String; + public static final fun getStringArrayT (Landroid/content/res/Resources;Lio/tolgee/Tolgee;I)[Ljava/lang/String; + public static final fun getStringT (Landroid/content/Context;I)Ljava/lang/String; + public static final fun getStringT (Landroid/content/Context;I[Ljava/lang/Object;)Ljava/lang/String; + public static final fun getStringT (Landroid/content/Context;Lio/tolgee/Tolgee;I)Ljava/lang/String; + public static final fun getStringT (Landroid/content/Context;Lio/tolgee/Tolgee;I[Ljava/lang/Object;)Ljava/lang/String; + public static final fun getTextT (Landroid/content/Context;Lio/tolgee/Tolgee;I)Ljava/lang/CharSequence; } public abstract interface class io/tolgee/model/TolgeeMessageParams : de/comahe/i18n4k/messages/formatter/MessageParameters { @@ -230,35 +247,15 @@ public final class io/tolgee/model/TolgeeMessageParams$None : io/tolgee/model/To public fun toString ()Ljava/lang/String; } -public final class io/tolgee/model/TolgeeProjectLanguage { - public static final field Companion Lio/tolgee/model/TolgeeProjectLanguage$Companion; - public final fun asLocale ()Ljava/util/Locale; - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/lang/String; - public final fun component4 ()Ljava/lang/String; - public final fun component5 ()Z - public fun equals (Ljava/lang/Object;)Z - public final fun getBase ()Z - public final fun getFlagEmoji ()Ljava/lang/String; - public final fun getName ()Ljava/lang/String; - public final fun getOriginalName ()Ljava/lang/String; - public final fun getTag ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public synthetic class io/tolgee/model/TolgeeProjectLanguage$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/tolgee/model/TolgeeProjectLanguage$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/tolgee/model/TolgeeProjectLanguage; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/tolgee/model/TolgeeProjectLanguage;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V +public abstract interface class io/tolgee/storage/TolgeeStorageProvider { + public abstract fun get (Ljava/lang/String;)[B + public abstract fun put (Ljava/lang/String;[B)V } -public final class io/tolgee/model/TolgeeProjectLanguage$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; +public final class io/tolgee/storage/TolgeeStorageProviderAndroid : io/tolgee/storage/TolgeeStorageProvider { + public fun (Landroid/content/Context;ILjava/lang/String;)V + public synthetic fun (Landroid/content/Context;ILjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun get (Ljava/lang/String;)[B + public fun put (Ljava/lang/String;[B)V } diff --git a/core/api/jvm/core.api b/core/api/jvm/core.api index 2538ca6a..688390e2 100644 --- a/core/api/jvm/core.api +++ b/core/api/jvm/core.api @@ -1,45 +1,43 @@ public class io/tolgee/Tolgee { public static final field Companion Lio/tolgee/Tolgee$Companion; public fun (Lio/tolgee/Tolgee$Config;)V + public final fun addChangeListener (Lio/tolgee/Tolgee$ChangeListener;)V + public final fun getChangeFlow ()Lkotlinx/coroutines/flow/MutableSharedFlow; public fun getConfig ()Lio/tolgee/Tolgee$Config; public static final fun getInstance ()Lio/tolgee/common/PlatformTolgee; + public static final fun getInstanceOrNull ()Lio/tolgee/common/PlatformTolgee; public fun getLocale ()Ljava/util/Locale; public static final fun getSystemLocale ()Ljava/util/Locale; - public static final fun init (Lio/tolgee/Tolgee$Config;)Lio/tolgee/common/PlatformTolgee; - public static final fun init (Lkotlin/jvm/functions/Function1;)Lio/tolgee/common/PlatformTolgee; - public static final fun init (ZLio/tolgee/Tolgee$Config;)Lio/tolgee/common/PlatformTolgee; - public static final fun init (ZLkotlin/jvm/functions/Function1;)Lio/tolgee/common/PlatformTolgee; - public static final fun instanceOrInit (Lio/tolgee/Tolgee$Config;)Lio/tolgee/common/PlatformTolgee; - public static final fun instanceOrInit (Lkotlin/jvm/functions/Function1;)Lio/tolgee/common/PlatformTolgee; - public static final fun instanceOrInit (ZLio/tolgee/Tolgee$Config;)Lio/tolgee/common/PlatformTolgee; - public static final fun instanceOrInit (ZLkotlin/jvm/functions/Function1;)Lio/tolgee/common/PlatformTolgee; - public final fun instant (Ljava/lang/String;)Ljava/lang/String; - public fun instant (Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;)Ljava/lang/String; - public static synthetic fun instant$default (Lio/tolgee/Tolgee;Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;ILjava/lang/Object;)Ljava/lang/String; + public static final fun init (Lio/tolgee/Tolgee$Config;)V + public static final fun init (Lkotlin/jvm/functions/Function1;)V + public static final fun new (Lio/tolgee/Tolgee$Config;)Lio/tolgee/common/PlatformTolgee; + public static final fun new (Lkotlin/jvm/functions/Function1;)Lio/tolgee/common/PlatformTolgee; public fun preload (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun setLocale (Lio/tolgee/model/TolgeeProjectLanguage;)Ljava/util/Locale; + public final fun removeChangeListener (Lio/tolgee/Tolgee$ChangeListener;)Z public fun setLocale (Ljava/lang/String;)Ljava/util/Locale; public fun setLocale (Ljava/util/Locale;)Ljava/util/Locale; - public final fun translation (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; - public fun translation (Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;)Lkotlinx/coroutines/flow/Flow; - public static synthetic fun translation$default (Lio/tolgee/Tolgee;Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; + public final fun t (Ljava/lang/String;)Ljava/lang/String; + public fun t (Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;)Ljava/lang/String; + public static synthetic fun t$default (Lio/tolgee/Tolgee;Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;ILjava/lang/Object;)Ljava/lang/String; + public fun tArray (Ljava/lang/String;)Ljava/util/List; + public fun tArrayFlow (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; + public final fun tFlow (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; + public fun tFlow (Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;)Lkotlinx/coroutines/flow/Flow; + public static synthetic fun tFlow$default (Lio/tolgee/Tolgee;Ljava/lang/String;Lio/tolgee/model/TolgeeMessageParams;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; +} + +public abstract interface class io/tolgee/Tolgee$ChangeListener { + public abstract fun onTranslationsChanged ()V } public final class io/tolgee/Tolgee$Companion { public final fun getInstance ()Lio/tolgee/common/PlatformTolgee; + public final fun getInstanceOrNull ()Lio/tolgee/common/PlatformTolgee; public final fun getSystemLocale ()Ljava/util/Locale; - public final fun init (Lio/tolgee/Tolgee$Config;)Lio/tolgee/common/PlatformTolgee; - public final fun init (Lkotlin/jvm/functions/Function1;)Lio/tolgee/common/PlatformTolgee; - public final fun init (ZLio/tolgee/Tolgee$Config;)Lio/tolgee/common/PlatformTolgee; - public final fun init (ZLkotlin/jvm/functions/Function1;)Lio/tolgee/common/PlatformTolgee; - public static synthetic fun init$default (Lio/tolgee/Tolgee$Companion;ZLio/tolgee/Tolgee$Config;ILjava/lang/Object;)Lio/tolgee/common/PlatformTolgee; - public static synthetic fun init$default (Lio/tolgee/Tolgee$Companion;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/tolgee/common/PlatformTolgee; - public final fun instanceOrInit (Lio/tolgee/Tolgee$Config;)Lio/tolgee/common/PlatformTolgee; - public final fun instanceOrInit (Lkotlin/jvm/functions/Function1;)Lio/tolgee/common/PlatformTolgee; - public final fun instanceOrInit (ZLio/tolgee/Tolgee$Config;)Lio/tolgee/common/PlatformTolgee; - public final fun instanceOrInit (ZLkotlin/jvm/functions/Function1;)Lio/tolgee/common/PlatformTolgee; - public static synthetic fun instanceOrInit$default (Lio/tolgee/Tolgee$Companion;ZLio/tolgee/Tolgee$Config;ILjava/lang/Object;)Lio/tolgee/common/PlatformTolgee; - public static synthetic fun instanceOrInit$default (Lio/tolgee/Tolgee$Companion;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/tolgee/common/PlatformTolgee; + public final fun init (Lio/tolgee/Tolgee$Config;)V + public final fun init (Lkotlin/jvm/functions/Function1;)V + public final fun new (Lio/tolgee/Tolgee$Config;)Lio/tolgee/common/PlatformTolgee; + public final fun new (Lkotlin/jvm/functions/Function1;)Lio/tolgee/common/PlatformTolgee; } public final class io/tolgee/Tolgee$Config { @@ -66,7 +64,6 @@ public final class io/tolgee/Tolgee$Config$Builder { public final fun getContentDelivery ()Lio/tolgee/Tolgee$Config$ContentDelivery; public final fun getLocale ()Ljava/util/Locale; public final fun getNetwork ()Lio/tolgee/Tolgee$Config$Network; - public final fun locale (Lio/tolgee/model/TolgeeProjectLanguage;)Lio/tolgee/Tolgee$Config$Builder; public final fun locale (Ljava/lang/String;)Lio/tolgee/Tolgee$Config$Builder; public final fun locale (Ljava/util/Locale;)Lio/tolgee/Tolgee$Config$Builder; public final fun network (Lio/tolgee/Tolgee$Config$Network;)Lio/tolgee/Tolgee$Config$Builder; @@ -83,9 +80,13 @@ public final class io/tolgee/Tolgee$Config$ContentDelivery { public static final field Companion Lio/tolgee/Tolgee$Config$ContentDelivery$Companion; public fun ()V public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Lio/tolgee/Tolgee$Formatter; + public final fun component2 ()Lkotlin/jvm/functions/Function1; + public final fun component3 ()Lio/tolgee/storage/TolgeeStorageProvider; + public final fun component4 ()Lio/tolgee/Tolgee$Formatter; public fun equals (Ljava/lang/Object;)Z public final fun getFormatter ()Lio/tolgee/Tolgee$Formatter; + public final fun getPath ()Lkotlin/jvm/functions/Function1; + public final fun getStorage ()Lio/tolgee/storage/TolgeeStorageProvider; public final fun getUrl ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -96,9 +97,15 @@ public final class io/tolgee/Tolgee$Config$ContentDelivery$Builder { public final fun build ()Lio/tolgee/Tolgee$Config$ContentDelivery; public final fun formatter (Lio/tolgee/Tolgee$Formatter;)Lio/tolgee/Tolgee$Config$ContentDelivery$Builder; public final fun getFormatter ()Lio/tolgee/Tolgee$Formatter; + public final fun getPath ()Lkotlin/jvm/functions/Function1; + public final fun getStorage ()Lio/tolgee/storage/TolgeeStorageProvider; public final fun getUrl ()Ljava/lang/String; + public final fun path (Lkotlin/jvm/functions/Function1;)Lio/tolgee/Tolgee$Config$ContentDelivery$Builder; public final fun setFormatter (Lio/tolgee/Tolgee$Formatter;)V + public final fun setPath (Lkotlin/jvm/functions/Function1;)V + public final fun setStorage (Lio/tolgee/storage/TolgeeStorageProvider;)V public final fun setUrl (Ljava/lang/String;)V + public final fun storage (Lio/tolgee/storage/TolgeeStorageProvider;)Lio/tolgee/Tolgee$Config$ContentDelivery$Builder; public final fun url (Ljava/lang/String;)Lio/tolgee/Tolgee$Config$ContentDelivery$Builder; } @@ -149,10 +156,6 @@ public final class io/tolgee/Tolgee$Formatter$Sprintf : io/tolgee/Tolgee$Formatt public fun toString ()Ljava/lang/String; } -public final class io/tolgee/common/ExtendCoroutinesKt { - public static final fun mapNotNull (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow; -} - public final class io/tolgee/common/PlatformTolgee : io/tolgee/Tolgee { public final fun component1 ()Lio/tolgee/Tolgee$Config; public fun equals (Ljava/lang/Object;)Z @@ -198,35 +201,8 @@ public final class io/tolgee/model/TolgeeMessageParams$None : io/tolgee/model/To public fun toString ()Ljava/lang/String; } -public final class io/tolgee/model/TolgeeProjectLanguage { - public static final field Companion Lio/tolgee/model/TolgeeProjectLanguage$Companion; - public final fun asLocale ()Ljava/util/Locale; - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/lang/String; - public final fun component4 ()Ljava/lang/String; - public final fun component5 ()Z - public fun equals (Ljava/lang/Object;)Z - public final fun getBase ()Z - public final fun getFlagEmoji ()Ljava/lang/String; - public final fun getName ()Ljava/lang/String; - public final fun getOriginalName ()Ljava/lang/String; - public final fun getTag ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public synthetic class io/tolgee/model/TolgeeProjectLanguage$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lio/tolgee/model/TolgeeProjectLanguage$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/tolgee/model/TolgeeProjectLanguage; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/tolgee/model/TolgeeProjectLanguage;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V -} - -public final class io/tolgee/model/TolgeeProjectLanguage$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; +public abstract interface class io/tolgee/storage/TolgeeStorageProvider { + public abstract fun get (Ljava/lang/String;)[B + public abstract fun put (Ljava/lang/String;[B)V } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 0bba6045..934af8be 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -15,7 +15,7 @@ plugins { signing } -val libGroup = "io.tolgee" +val libGroup = "io.tolgee.mobile-kotlin-sdk" val libName = "core" val appleFramework = "Tolgee" @@ -27,7 +27,7 @@ dokka { dokkaSourceSets.configureEach { sourceLink { localDirectory.set(file("src")) - remoteUrl("https://github.com/tolgee/compose-tolgee/tree/master/core/src") + remoteUrl("https://github.com/tolgee/tolgee-mobile-kotlin-sdk/tree/master/core/src") } } } @@ -54,7 +54,7 @@ kotlin { name = appleFramework version = libVersion license = "Apache License 2.0" - homepage = "https://github.com/tolgee/compose-tolgee" + homepage = "https://github.com/tolgee/tolgee-mobile-kotlin-sdk" summary = "Kotlin Multiplatform localization wrapper for Tolgee" framework { @@ -233,7 +233,7 @@ kotlin { android { compileSdk = 35 - namespace = "io.tolgee.core" + namespace = "io.tolgee.mobilekotlinsdk.core" defaultConfig { minSdk = 21 @@ -259,7 +259,7 @@ mavenPublishing { name.set(libName) description.set("Kotlin Multiplatform localization wrapper for Tolgee") - url.set("https://github.com/tolgee/compose-tolgee") + url.set("https://github.com/tolgee/tolgee-mobile-kotlin-sdk") licenses { license { @@ -269,8 +269,8 @@ mavenPublishing { } scm { - url.set("https://github.com/tolgee/compose-tolgee") - connection.set("scm:git:git://github.com/tolgee/compose-tolgee.git") + url.set("https://github.com/tolgee/tolgee-mobile-kotlin-sdk") + connection.set("scm:git:git://github.com/tolgee/tolgee-mobile-kotlin-sdk.git") } developers { diff --git a/core/src/androidMain/kotlin/io/tolgee/TolgeeAndroid.kt b/core/src/androidMain/kotlin/io/tolgee/TolgeeAndroid.kt index fbccf816..7aa6a9b5 100644 --- a/core/src/androidMain/kotlin/io/tolgee/TolgeeAndroid.kt +++ b/core/src/androidMain/kotlin/io/tolgee/TolgeeAndroid.kt @@ -8,11 +8,10 @@ import androidx.annotation.PluralsRes import androidx.annotation.StringRes import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope -import io.tolgee.common.mapNotNull import io.tolgee.model.TolgeeMessageParams -import dev.datlag.tooling.async.scopeCatching import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch @@ -41,11 +40,11 @@ data class TolgeeAndroid internal constructor( * @return A [Flow] emitting localized text, starting with the Android string resource and * followed by the corresponding Tolgee translations, if available. */ - fun translation(context: Context, @StringRes id: Int): Flow = flow { - emit(instant(context, id)) + fun tFlow(context: Context, @StringRes id: Int): Flow = flow { + emit(t(context, id)) getKeyFromResources(context, id)?.let { key -> - emitAll(translation(key, TolgeeMessageParams.None).mapNotNull()) + emitAll(tFlow(key, TolgeeMessageParams.None).filterNotNull()) } } @@ -59,35 +58,35 @@ data class TolgeeAndroid internal constructor( * @param formatArgs optional arguments to format the string resource * @return a flow of translated strings */ - fun translation(context: Context, @StringRes id: Int, vararg formatArgs: Any): Flow = flow { - emit(instant(context, id, *formatArgs)) + fun tFlow(context: Context, @StringRes id: Int, vararg formatArgs: Any): Flow = flow { + emit(t(context, id, *formatArgs)) getKeyFromResources(context, id)?.let { key -> - emitAll(translation(key, TolgeeMessageParams.Indexed(*formatArgs))) + emitAll(tFlow(key, TolgeeMessageParams.Indexed(*formatArgs))) } } - fun pluralTranslation(resources: Resources, @PluralsRes id: Int, quantity: Int): Flow = flow { - emit(pluralInstant(resources, id, quantity)) + fun tPluralFlow(resources: Resources, @PluralsRes id: Int, quantity: Int): Flow = flow { + emit(tPlural(resources, id, quantity)) getKeyFromResources(resources, id)?.let { key -> - emitAll(translation(key, TolgeeMessageParams.Indexed(quantity))) + emitAll(tFlow(key, TolgeeMessageParams.Indexed(quantity))) } } - fun pluralTranslation(resources: Resources, @PluralsRes id: Int, quantity: Int, vararg formatArgs: Any): Flow = flow { - emit(pluralInstant(resources, id, quantity, *formatArgs)) + fun tPluralFlow(resources: Resources, @PluralsRes id: Int, quantity: Int, vararg formatArgs: Any): Flow = flow { + emit(tPlural(resources, id, quantity, *formatArgs)) getKeyFromResources(resources, id)?.let { key -> - emitAll(translation(key, TolgeeMessageParams.Indexed(quantity, *formatArgs))) + emitAll(tFlow(key, TolgeeMessageParams.Indexed(quantity, *formatArgs))) } } - fun stringArrayTranslation(resources: Resources, @ArrayRes id: Int): Flow> = flow { - emit(stringArrayInstant(resources, id)) + fun tArrayFlow(resources: Resources, @ArrayRes id: Int): Flow> = flow { + emit(tArray(resources, id)) getKeyFromResources(resources, id)?.let { key -> - emitAll(stringArrayTranslation(key).mapNotNull { it.ifEmpty { null } }) + emitAll(tArrayFlow(key).mapNotNull { it.ifEmpty { null } }) } } @@ -100,9 +99,9 @@ data class TolgeeAndroid internal constructor( * @param id The resource ID of the string to be translated. * @return The translated string if a key-based translation is found; otherwise, the default string resource value. */ - fun instant(context: Context, @StringRes id: Int): String { + fun t(context: Context, @StringRes id: Int): String { return getKeyFromResources(context, id)?.let { key -> - instant(key) + t(key) } ?: context.getString(id) } @@ -116,30 +115,50 @@ data class TolgeeAndroid internal constructor( * @param formatArgs Optional arguments for formatting the string. * @return The translated string if a translation key is found, or the default string resource value. */ - fun instant(context: Context, @StringRes id: Int, vararg formatArgs: Any): String { + fun t(context: Context, @StringRes id: Int, vararg formatArgs: Any): String { return getKeyFromResources(context, id)?.let { key -> - instant(key, TolgeeMessageParams.Indexed(*formatArgs)) + t(key, TolgeeMessageParams.Indexed(*formatArgs)) } ?: context.getString(id, *formatArgs) } - fun pluralInstant(resources: Resources, @PluralsRes id: Int, quantity: Int): String { + fun tPlural(resources: Resources, @PluralsRes id: Int, quantity: Int): String { return getKeyFromResources(resources, id)?.let { key -> - instant(key, TolgeeMessageParams.Indexed(quantity)) - } ?: resources.getQuantityString(id, quantity) + t(key, TolgeeMessageParams.Indexed(quantity)) + } ?: resources.getQuantityString(id, quantity, quantity) } - fun pluralInstant(resources: Resources, @PluralsRes id: Int, quantity: Int, vararg formatArgs: Any): String { + fun tPlural(resources: Resources, @PluralsRes id: Int, quantity: Int, vararg formatArgs: Any): String { return getKeyFromResources(resources, id)?.let { key -> - instant(key, TolgeeMessageParams.Indexed(quantity, *formatArgs)) - } ?: resources.getQuantityString(id, quantity, *formatArgs) + t(key, TolgeeMessageParams.Indexed(quantity, *formatArgs)) + } ?: resources.getQuantityString(id, quantity, quantity, *formatArgs) } - fun stringArrayInstant(resources: Resources, @ArrayRes id: Int): List { + fun tArray(resources: Resources, @ArrayRes id: Int): List { return getKeyFromResources(resources, id)?.let { key -> - stringArrayInstant(key) + tArray(key) }?.ifEmpty { null } ?: resources.getStringArray(id).toList() } + /** + * Provides an immediate translation for the given string resource ID within the given context. + * If a translation key is derived from the string resource, it retrieves the translation using Tolgee. + * Otherwise, it falls back to returning the default string resource value. + * + * This is a special version that allows returning a [CharSequence] instead of a [String] and + * will fall back to the Android `getText` method if no translation is found - preserving formatting. + * + * If translation is found, no style information is preserved and the method acts the same as [t]. + * + * @param context The context used to access resources and provide localization settings. + * @param id The resource ID of the string to be translated. + * @return The translated string if a key-based translation is found; otherwise, the default string resource value. + */ + fun tStyled(context: Context, @StringRes id: Int): CharSequence { + return getKeyFromResources(context, id)?.let { key -> + t(key) + } ?: context.getText(id) + } + /** * Preloads the required languages and their translations for the current Tolgee instance. * @@ -147,7 +166,7 @@ data class TolgeeAndroid internal constructor( * corresponding translations are loaded into memory. It performs these operations atomically * by utilizing mutex locks to prevent concurrent modifications. * - * Must be called before accessing translation functionalities such as [instant] to ensure + * Must be called before accessing translation functionalities such as [t] to ensure * that translations are available and up-to-date. * * This method is coroutine-safe and utilizes structured concurrency to manage asynchronous diff --git a/core/src/androidMain/kotlin/io/tolgee/TolgeeContextWrapper.kt b/core/src/androidMain/kotlin/io/tolgee/TolgeeContextWrapper.kt index 36e4bafc..bc297a2b 100644 --- a/core/src/androidMain/kotlin/io/tolgee/TolgeeContextWrapper.kt +++ b/core/src/androidMain/kotlin/io/tolgee/TolgeeContextWrapper.kt @@ -49,7 +49,7 @@ class TolgeeContextWrapper( */ @JvmStatic fun wrap(base: Context?): ContextWrapper { - val tolgee = Tolgee.instance ?: return ContextWrapper(base) + val tolgee = Tolgee.instanceOrNull ?: return ContextWrapper(base) return wrap(base, tolgee) } diff --git a/core/src/androidMain/kotlin/io/tolgee/TolgeeResourceNameCache.kt b/core/src/androidMain/kotlin/io/tolgee/TolgeeResourceNameCache.kt index 10804cad..88a4e74b 100644 --- a/core/src/androidMain/kotlin/io/tolgee/TolgeeResourceNameCache.kt +++ b/core/src/androidMain/kotlin/io/tolgee/TolgeeResourceNameCache.kt @@ -13,11 +13,9 @@ internal object TolgeeResourceNameCache { fun getEntryName(resources: Resources, @AnyRes resId: Int): String? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - computeEntryName(resources, resId)?.let { return it } + return computeEntryName(resources, resId) } - return accessEntryName(resources, resId) ?: scopeCatching { - resources.getResourceEntryName(resId) - }.getOrNull()?.ifBlank { null } + return computeEntryNameFallback(resources, resId) } @RequiresApi(Build.VERSION_CODES.N) @@ -29,13 +27,18 @@ internal object TolgeeResourceNameCache { }.getOrNull()?.ifBlank { null } } - private fun accessEntryName(resources: Resources, @AnyRes resId: Int): String? { - cache[resId]?.ifBlank { null }?.let { return it } - - val name = scopeCatching { - resources.getResourceEntryName(resId) - }.getOrNull()?.ifBlank { null } ?: return null + private fun computeEntryNameFallback(resources: Resources, @AnyRes resId: Int): String? { + // computeIfAbsent not available for API < N (24) + return scopeCatching { + cache.computeIfAbsentFallback(resId) { + resources.getResourceEntryName(it) + } + }.getOrNull()?.ifBlank { null } + } - return cache.putIfAbsent(resId, name)?.ifBlank { null } ?: name + private fun ConcurrentHashMap.computeIfAbsentFallback(key: K, mappingFunction: (K) -> V): V { + get(key)?.let { return it } + val name = mappingFunction(key) + return putIfAbsent(key, name) ?: name } } \ No newline at end of file diff --git a/core/src/androidMain/kotlin/io/tolgee/TolgeeResources.kt b/core/src/androidMain/kotlin/io/tolgee/TolgeeResources.kt index d9b8ab98..1e579a60 100644 --- a/core/src/androidMain/kotlin/io/tolgee/TolgeeResources.kt +++ b/core/src/androidMain/kotlin/io/tolgee/TolgeeResources.kt @@ -2,9 +2,10 @@ package io.tolgee import android.content.Context import android.content.res.Resources -import io.tolgee.common.getQuantityStringInstant -import io.tolgee.common.getStringArrayInstant -import io.tolgee.common.getStringInstant +import io.tolgee.common.getQuantityStringT +import io.tolgee.common.getStringArrayT +import io.tolgee.common.getStringT +import io.tolgee.common.getTextT /** * Ignore Deprecation: Resources constructor is not really deprecated, apps should just not create @@ -19,22 +20,26 @@ internal class TolgeeResources( ) : Resources(base.assets, base.displayMetrics, base.configuration) { override fun getString(id: Int): String { - return baseContext.getStringInstant(tolgee, id) + return baseContext.getStringT(tolgee, id) } override fun getString(id: Int, vararg formatArgs: Any?): String { - return baseContext.getStringInstant(tolgee, id, *formatArgs.filterNotNull().toTypedArray()) + return baseContext.getStringT(tolgee, id, *formatArgs.filterNotNull().toTypedArray()) } override fun getQuantityString(id: Int, quantity: Int): String { - return baseContext.resources.getQuantityStringInstant(tolgee, id, quantity) + return baseContext.resources.getQuantityStringT(tolgee, id, quantity) } override fun getQuantityString(id: Int, quantity: Int, vararg formatArgs: Any?): String { - return baseContext.resources.getQuantityStringInstant(tolgee, id, quantity, *formatArgs.filterNotNull().toTypedArray()) + return baseContext.resources.getQuantityStringT(tolgee, id, quantity, *formatArgs.filterNotNull().toTypedArray()) } override fun getStringArray(id: Int): Array { - return baseContext.resources.getStringArrayInstant(tolgee, id) + return baseContext.resources.getStringArrayT(tolgee, id) + } + + override fun getText(id: Int): CharSequence { + return baseContext.getTextT(tolgee, id) } } \ No newline at end of file diff --git a/core/src/androidMain/kotlin/io/tolgee/common/ExtendTolgee.android.kt b/core/src/androidMain/kotlin/io/tolgee/common/ExtendTolgee.android.kt index 135ab7e9..7fe0e126 100644 --- a/core/src/androidMain/kotlin/io/tolgee/common/ExtendTolgee.android.kt +++ b/core/src/androidMain/kotlin/io/tolgee/common/ExtendTolgee.android.kt @@ -11,6 +11,7 @@ import io.tolgee.model.TolgeeMessageParams import io.ktor.client.* import io.ktor.client.engine.android.* import io.ktor.client.plugins.cache.* +import io.tolgee.storage.TolgeeStorageProvider import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext @@ -60,10 +61,10 @@ internal actual val platformNetworkContext: CoroutineContext * @return The string data associated with the resource, formatted and * stripped of styled text information. */ -fun Context.getStringInstant(tolgee: Tolgee, @StringRes resId: Int): String { - return (tolgee as? TolgeeAndroid)?.instant(this, resId) +fun Context.getStringT(tolgee: Tolgee, @StringRes resId: Int): String { + return (tolgee as? TolgeeAndroid)?.t(this, resId) ?: TolgeeAndroid.getKeyFromResources(this, resId)?.let { - tolgee.instant(key = it, parameters = TolgeeMessageParams.None) + tolgee.t(key = it, parameters = TolgeeMessageParams.None) } ?: this.getString(resId) } @@ -76,9 +77,9 @@ fun Context.getStringInstant(tolgee: Tolgee, @StringRes resId: Int): String { * @return The string data associated with the resource, formatted and * stripped of styled text information. */ -fun Context.getStringInstant(@StringRes resId: Int): String { - val instance = Tolgee.instance ?: return this.getString(resId) - return this.getStringInstant(instance, resId) +fun Context.getStringT(@StringRes resId: Int): String { + val instance = Tolgee.instanceOrNull ?: return this.getString(resId) + return this.getStringT(instance, resId) } /** @@ -93,10 +94,10 @@ fun Context.getStringInstant(@StringRes resId: Int): String { * @return The string data associated with the resource, formatted and * stripped of styled text information. */ -fun Context.getStringInstant(tolgee: Tolgee, @StringRes resId: Int, vararg formatArgs: Any): String { - return (tolgee as? TolgeeAndroid)?.instant(this, resId, *formatArgs) +fun Context.getStringT(tolgee: Tolgee, @StringRes resId: Int, vararg formatArgs: Any): String { + return (tolgee as? TolgeeAndroid)?.t(this, resId, *formatArgs) ?: TolgeeAndroid.getKeyFromResources(this, resId)?.let { - tolgee.instant(key = it, parameters = TolgeeMessageParams.Indexed(*formatArgs)) + tolgee.t(key = it, parameters = TolgeeMessageParams.Indexed(*formatArgs)) } ?: this.getString(resId, *formatArgs) } @@ -111,43 +112,66 @@ fun Context.getStringInstant(tolgee: Tolgee, @StringRes resId: Int, vararg forma * @return The string data associated with the resource, formatted and * stripped of styled text information. */ -fun Context.getStringInstant(@StringRes resId: Int, vararg formatArgs: Any): String { - val instance = Tolgee.instance ?: return this.getString(resId, *formatArgs) - return this.getStringInstant(instance, resId, *formatArgs) +fun Context.getStringT(@StringRes resId: Int, vararg formatArgs: Any): String { + val instance = Tolgee.instanceOrNull ?: return this.getString(resId, *formatArgs) + return this.getStringT(instance, resId, *formatArgs) } -fun Resources.getQuantityStringInstant(tolgee: Tolgee, @PluralsRes resId: Int, quantity: Int): String { - return (tolgee as? TolgeeAndroid)?.pluralInstant(this, resId, quantity) +fun Resources.getQuantityStringT(tolgee: Tolgee, @PluralsRes resId: Int, quantity: Int): String { + return (tolgee as? TolgeeAndroid)?.tPlural(this, resId, quantity) ?: TolgeeAndroid.getKeyFromResources(this, resId)?.let { - tolgee.instant(key = it, TolgeeMessageParams.Indexed(quantity)) + tolgee.t(key = it, TolgeeMessageParams.Indexed(quantity)) } ?: this.getQuantityString(resId, quantity) } -fun Resources.getQuantityStringInstant(tolgee: Tolgee, @PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any): String { - return (tolgee as? TolgeeAndroid)?.pluralInstant(this, resId, quantity, *formatArgs) +fun Resources.getQuantityStringT(tolgee: Tolgee, @PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any): String { + return (tolgee as? TolgeeAndroid)?.tPlural(this, resId, quantity, *formatArgs) ?: TolgeeAndroid.getKeyFromResources(this, resId)?.let { - tolgee.instant(key = it, TolgeeMessageParams.Indexed(quantity, *formatArgs)) + tolgee.t(key = it, TolgeeMessageParams.Indexed(quantity, *formatArgs)) } ?: this.getQuantityString(resId, quantity, *formatArgs) } -fun Resources.getQuantityStringInstant(@PluralsRes resId: Int, quantity: Int): String { - val instance = Tolgee.instance ?: return this.getQuantityString(resId, quantity) - return this.getQuantityStringInstant(instance, resId, quantity) +fun Resources.getQuantityStringT(@PluralsRes resId: Int, quantity: Int): String { + val instance = Tolgee.instanceOrNull ?: return this.getQuantityString(resId, quantity) + return this.getQuantityStringT(instance, resId, quantity) } -fun Resources.getQuantityStringInstant(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any): String { - val instance = Tolgee.instance ?: return this.getQuantityString(resId, quantity, *formatArgs) - return this.getQuantityStringInstant(instance, resId, quantity, *formatArgs) +fun Resources.getQuantityStringT(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any): String { + val instance = Tolgee.instanceOrNull ?: return this.getQuantityString(resId, quantity, *formatArgs) + return this.getQuantityStringT(instance, resId, quantity, *formatArgs) } -fun Resources.getStringArrayInstant(tolgee: Tolgee, @ArrayRes resId: Int): Array { - val list = (tolgee as? TolgeeAndroid)?.stringArrayInstant(this, resId) +fun Resources.getStringArrayT(tolgee: Tolgee, @ArrayRes resId: Int): Array { + val list = (tolgee as? TolgeeAndroid)?.tArray(this, resId) ?: TolgeeAndroid.getKeyFromResources(this, resId)?.let { - tolgee.stringArrayInstant(key = it) + tolgee.tArray(key = it) } return list?.ifEmpty { null }?.toTypedArray() ?: this.getStringArray(resId) } +/** + * Returns a localized formatted string from [Tolgee] cache or the application's package's + * default string table, substituting the format arguments as defined in + * [java.util.Formatter] and [java.lang.String.format]. + * + * This function will fall back to the Android `getText` method if no translation is found - preserving formatting. + * If translation is found, no style information is preserved and the method acts the same as [getStringT]. + * + * @param tolgee The [Tolgee] instance to get the cached string from. + * @param resId Resource id for the format string + * @return The string data associated with the resource, formatted and + * stripped of styled text information. + */ +fun Context.getTextT(tolgee: Tolgee, @StringRes resId: Int): CharSequence { + return (tolgee as? TolgeeAndroid)?.tStyled(this, resId) + ?: TolgeeAndroid.getKeyFromResources(this, resId)?.let { + tolgee.t(key = it, parameters = TolgeeMessageParams.None) + } ?: this.getText(resId) +} + +internal actual val platformStorage: TolgeeStorageProvider? + get() = null + /** * Typealias representing a platform-specific implementation of the Tolgee class for Android. * diff --git a/core/src/androidMain/kotlin/io/tolgee/storage/TolgeeStorageProviderAndroid.kt b/core/src/androidMain/kotlin/io/tolgee/storage/TolgeeStorageProviderAndroid.kt new file mode 100644 index 00000000..6a151707 --- /dev/null +++ b/core/src/androidMain/kotlin/io/tolgee/storage/TolgeeStorageProviderAndroid.kt @@ -0,0 +1,48 @@ +package io.tolgee.storage + +import android.content.Context +import dev.datlag.tooling.canReadSafely +import dev.datlag.tooling.canWriteSafely +import dev.datlag.tooling.existsSafely +import dev.datlag.tooling.mkdirsSafely +import dev.datlag.tooling.scopeCatching +import java.io.File + +class TolgeeStorageProviderAndroid( + private val context: Context, + private val versionCode: Int, + private val path: String = "tolgee/localization-cache", +) : TolgeeStorageProvider { + private val cacheDir get() = context.filesDir / path / versionCode.toString() + + override fun put(name: String, data: ByteArray) { + val dir = cacheDir + dir.mkdirsSafely() + val file = dir / escape(name) + if (!file.canWriteSafely()) { + return + } + scopeCatching { + file.writeBytes(data) + } + } + + override fun get(name: String): ByteArray? { + val file = cacheDir / escape(name) + if (!file.existsSafely() || !file.canReadSafely()) { + return null + } + + return scopeCatching { + file.readBytes() + }.getOrNull() + } + + private fun escape(name: String): String { + return name.replace("/", "_") + } + + internal companion object { + private infix operator fun File.div(other: String) = File(this, other) + } +} \ No newline at end of file diff --git a/core/src/appleMain/kotlin/io/tolgee/TolgeeApple.kt b/core/src/appleMain/kotlin/io/tolgee/TolgeeApple.kt index 33a9f228..37bd1615 100644 --- a/core/src/appleMain/kotlin/io/tolgee/TolgeeApple.kt +++ b/core/src/appleMain/kotlin/io/tolgee/TolgeeApple.kt @@ -3,11 +3,11 @@ package io.tolgee import com.rickclephas.kmp.nativecoroutines.NativeCoroutines import de.comahe.i18n4k.createLocale import io.tolgee.common.fromRes -import io.tolgee.common.mapNotNull import io.tolgee.model.TolgeeMessageParams import io.tolgee.common.localizedString import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flow import platform.Foundation.NSBundle import platform.Foundation.NSLocale @@ -45,7 +45,7 @@ data class TolgeeApple internal constructor( * @return A flow that emits localized strings corresponding to the given key. */ @NativeCoroutines - fun translation(key: String, default: String?, table: String? = null): Flow = flow { + fun tFlow(key: String, default: String?, table: String? = null): Flow = flow { emit( getLocalizedStringFromBundle( bundleRes, @@ -55,8 +55,8 @@ data class TolgeeApple internal constructor( ) ?: default?.ifBlank { null } ) - emitAll(translation(key, TolgeeMessageParams.None)) - }.mapNotNull() + emitAll(tFlow(key, TolgeeMessageParams.None)) + }.filterNotNull() /** * Provides a flow of localized string representations for a given key, allowing localization updates @@ -72,11 +72,11 @@ data class TolgeeApple internal constructor( * @return A flow that emits localized and formatted string values corresponding to the given key and arguments. */ @NativeCoroutines - fun translation(key: String, default: String?, table: String? = null, vararg args: Any): Flow = flow { + fun tFlow(key: String, default: String?, table: String? = null, vararg args: Any): Flow = flow { emit(getLocalizedStringFromBundleFormatted(bundleRes, key, default, table, *args)) - emitAll(translation(key, TolgeeMessageParams.Indexed(*args))) - }.mapNotNull() + emitAll(tFlow(key, TolgeeMessageParams.Indexed(*args))) + }.filterNotNull() /** * Retrieves the translation for a given key immediately. @@ -87,8 +87,8 @@ data class TolgeeApple internal constructor( * @param table The optional localization table where the key is searched. Can be null. * @return The localized string if found, or the default value if provided. Returns null if none is available. */ - fun instant(key: String, default: String?, table: String? = null): String? { - return instant(key) ?: getLocalizedStringFromBundle( + fun t(key: String, default: String?, table: String? = null): String? { + return t(key) ?: getLocalizedStringFromBundle( bundleRes, key, default, @@ -108,8 +108,8 @@ data class TolgeeApple internal constructor( * @param args Optional arguments to be used for formatting the translation string. * @return The translated and optionally formatted string, or null if no translation is found and no default is provided. */ - fun instant(key: String, default: String?, table: String? = null, vararg args: Any): String? { - return instant(key, TolgeeMessageParams.Indexed(*args)) + fun t(key: String, default: String?, table: String? = null, vararg args: Any): String? { + return t(key, TolgeeMessageParams.Indexed(*args)) ?: getLocalizedStringFromBundleFormatted(bundleRes, key, default, table, *args) } diff --git a/core/src/appleMain/kotlin/io/tolgee/common/ExtendTolgee.apple.kt b/core/src/appleMain/kotlin/io/tolgee/common/ExtendTolgee.apple.kt index 5c119bd0..c8089776 100644 --- a/core/src/appleMain/kotlin/io/tolgee/common/ExtendTolgee.apple.kt +++ b/core/src/appleMain/kotlin/io/tolgee/common/ExtendTolgee.apple.kt @@ -6,6 +6,7 @@ import io.tolgee.TolgeeApple import io.ktor.client.* import io.ktor.client.engine.darwin.Darwin import io.ktor.client.plugins.cache.* +import io.tolgee.storage.TolgeeStorageProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import platform.Foundation.NSLocale @@ -47,6 +48,9 @@ internal actual fun createPlatformTolgee(config: Tolgee.Config): PlatformTolgee internal actual val platformNetworkContext: CoroutineContext get() = Dispatchers.IO +internal actual val platformStorage: TolgeeStorageProvider? + get() = null + /** * Provides a platform-specific type alias for the Tolgee localization framework implementation. * On Apple platforms, `PlatformTolgee` is resolved to `TolgeeApple`, which handles localization diff --git a/core/src/commonMain/kotlin/io/tolgee/Tolgee.kt b/core/src/commonMain/kotlin/io/tolgee/Tolgee.kt index 2d397ee4..d0c1edd2 100644 --- a/core/src/commonMain/kotlin/io/tolgee/Tolgee.kt +++ b/core/src/commonMain/kotlin/io/tolgee/Tolgee.kt @@ -11,16 +11,18 @@ import io.tolgee.Tolgee.Companion.systemLocale import io.tolgee.api.TolgeeApi import io.tolgee.common.PlatformTolgee import io.tolgee.common.createPlatformTolgee -import io.tolgee.common.mapNotNull import io.tolgee.common.platformHttpClient import io.tolgee.common.platformNetworkContext +import io.tolgee.common.platformStorage import io.tolgee.model.TolgeeMessageParams -import io.tolgee.model.TolgeeProjectLanguage import io.tolgee.model.TolgeeTranslation +import io.tolgee.storage.TolgeeStorageProvider import kotlinx.atomicfu.atomic import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.sync.Mutex @@ -51,9 +53,8 @@ import kotlin.jvm.JvmStatic * - `config`: Configuration settings for the Tolgee instance. * * Methods: - * - `languages`: Retrieves a list of available languages, preferring the network source with a fallback to cache. - * - `translation`: Processes and streams dynamic translations for a given key and parameters. - * - `instant`: Immediately resolves a translation for a given key and parameters. + * - `tFlow`: Processes and streams dynamic translations for a given key and parameters. + * - `t`: Immediately resolves a translation for a given key and parameters. * - `preload`: Preloads languages and translations into memory for subsequent use. * - `setLocale`: Updates the current locale using various parameter types. */ @@ -97,6 +98,52 @@ open class Tolgee( MutableStateFlow(config.locale) } + /** + * A flow that emits whenever translations change. + * + * This can be used in to reactively respond to translation changes. + */ + val changeFlow by lazy { + MutableSharedFlow() + } + + /** + * Interface for listening to changes in Tolgee translations. + * + * This interface provides a callback mechanism for Java code and other environments + * where Kotlin Flows might not be the preferred approach for handling asynchronous events. + */ + interface ChangeListener { + /** + * Called when translations in Tolgee have changed. + */ + fun onTranslationsChanged() + } + + /** + * Collection of registered change listeners. + */ + private val changeListeners = mutableSetOf() + + /** + * Registers a listener to be notified when translations change. + * + * @param listener The listener to register + */ + fun addChangeListener(listener: ChangeListener) { + changeListeners.add(listener) + } + + /** + * Unregisters a previously registered change listener. + * + * @param listener The listener to unregister + * @return true if the listener was found and removed, false otherwise + */ + fun removeChangeListener(listener: ChangeListener): Boolean { + return changeListeners.remove(listener) + } + /** * Loads translations for a given locale or the default locale if none is specified. @@ -117,6 +164,10 @@ open class Tolgee( currentLanguage = locale?.language?.ifBlank { null }, ).also { cachedTranslation.value = it + changeFlow.emit(Unit) + changeListeners.forEach { listener -> + listener.onTranslationsChanged() + } } } } @@ -141,46 +192,46 @@ open class Tolgee( } /** - * Updating Tolgee translation for key with parameters. + * Updating Tolgee translation for a key with parameters. * * Respects locale changes from [setLocale]. */ @JvmOverloads @OptIn(ExperimentalCoroutinesApi::class) @NativeCoroutines - open fun translation( + open fun tFlow( key: String, parameters: TolgeeMessageParams = TolgeeMessageParams.None ): Flow = localeFlow.mapLatest { locale -> val translation = currentTranslation(locale) ?: suspendCatching { loadTranslations(locale) - }.getOrNull() ?: currentTranslation(locale) ?: return@mapLatest instant(key, parameters) + }.getOrNull() ?: currentTranslation(locale) ?: return@mapLatest t(key, parameters) return@mapLatest translation.localized(key, parameters, locale) - }.mapNotNull() + }.filterNotNull() @OptIn(ExperimentalCoroutinesApi::class) @NativeCoroutines - open fun stringArrayTranslation( + open fun tArrayFlow( key: String ): Flow> = localeFlow.mapLatest { locale -> val translation = currentTranslation(locale) ?: suspendCatching { loadTranslations(locale) - }.getOrNull() ?: currentTranslation(locale) ?: return@mapLatest stringArrayInstant(key) + }.getOrNull() ?: currentTranslation(locale) ?: return@mapLatest tArray(key) return@mapLatest translation.stringArray(key, locale) - }.mapNotNull() + }.filterNotNull() /** - * Immediate Tolgee translation for key with parameters. + * Immediate Tolgee translation for a key with parameters. * - * **Requires** calling [preload] or [translation] before. + * **Requires** calling [preload] or [tFlow] before. * (Otherwise) May return null if no translations are loaded at time calling. * * Respects only the locale at time calling. */ @JvmOverloads - open fun instant( + open fun t( key: String, parameters: TolgeeMessageParams = TolgeeMessageParams.None ): String? { @@ -189,7 +240,7 @@ open class Tolgee( return translation.localized(key, parameters, localeFlow.value) } - open fun stringArrayInstant( + open fun tArray( key: String ): List { val translation = currentTranslation() ?: return emptyList() @@ -204,7 +255,7 @@ open class Tolgee( * corresponding translations are loaded into memory. It performs these operations atomically * by utilizing mutex locks to prevent concurrent modifications. * - * Must be called before accessing translation functionalities such as [instant] to ensure + * Must be called before accessing translation functionalities such as [t] to ensure * that translations are available and up-to-date. * * This method is coroutine-safe and utilizes structured concurrency to manage asynchronous @@ -220,6 +271,7 @@ open class Tolgee( * * @param locale The locale to be set for translations and related operations. */ + @JvmOverloads open fun setLocale(locale: Locale) = localeFlow.updateAndGet { locale } ?: locale /** @@ -227,19 +279,9 @@ open class Tolgee( * * @param localeTag A string representation of the desired locale. */ + @JvmOverloads open fun setLocale(localeTag: String) = setLocale(forLocaleTag(localeTag)) - /** - * Sets the current locale using the specified language configuration. - * - * This method updates the locale by converting the provided language configuration - * to a `Locale` instance. - * - * @param language The `TolgeeProjectLanguage` instance representing the language configuration - * to set as the current locale. - */ - open fun setLocale(language: TolgeeProjectLanguage) = setLocale(language.asLocale()) - /** * Gets the current locale for the Tolgee instance. * @@ -320,17 +362,6 @@ open class Tolgee( */ fun locale(localeTag: String) = locale(forLocaleTag(localeTag)) - /** - * Configures the locale for the builder using a `TolgeeProjectLanguage` instance. - * - * This function converts the given `TolgeeProjectLanguage` into a `Locale` using its `asLocale()` - * method and applies the resulting `Locale` to the builder configuration. - * - * @param language The `TolgeeProjectLanguage` instance representing the language configuration - * that will be converted into a `Locale` and applied to the builder. - */ - fun locale(language: TolgeeProjectLanguage) = locale(language.asLocale()) - /** * Configures the network settings for the builder. * @@ -526,7 +557,9 @@ open class Tolgee( @ConsistentCopyVisibility data class ContentDelivery internal constructor( val url: String? = null, - val formatter: Formatter = Formatter.ICU + val path: (language: String) -> String = { "$it.json" }, + val storage: TolgeeStorageProvider? = platformStorage, + val formatter: Formatter = Formatter.Sprintf, ) { /** * A builder class for constructing instances of `CDN` with configurable properties. @@ -543,6 +576,32 @@ open class Tolgee( */ var url: String? = null + /** + * Defines the path generation logic for localization files within the CDN configuration. + * + * This variable is a lambda function that takes a language code as input and returns + * the corresponding file path as a string. The default implementation appends ".json" + * to the supplied language code to generate the path. + * + * @property language The language code for which the path is being generated (e.g., "en"). + * @return The generated file path, typically in the format `.json`. + */ + var path: (language: String) -> String = { "$it.json" } + + /** + * Represents the storage configuration for the Builder. + * + * This property allows the customization of the storage mechanism by providing an implementation of + * the `TolgeeStorageProvider` interface. The `TolgeeStorageProvider` interface defines methods for storing and retrieving + * data, enabling support for different storage backends. + * + * By default, it is initialized with `platformStorage`, which can be replaced with a custom implementation + * through the `storage(storage: TolgeeStorageProvider)` method in the Builder. + * + * @property storage The `TolgeeStorageProvider` instance used to handle the storage of data. + */ + var storage: TolgeeStorageProvider? = platformStorage + /** * Specifies the formatting strategy to be used for dynamic text translations. * @@ -554,7 +613,7 @@ open class Tolgee( * Typically used within the `CDN.Builder` class to configure translation formatting behavior * for the resulting `CDN` instance. */ - var formatter: Formatter = Formatter.ICU + var formatter: Formatter = Formatter.Sprintf /** * Sets the URL for the CDN configuration. @@ -566,6 +625,34 @@ open class Tolgee( this.url = url } + /** + * Sets the path for the CDN configuration based on the provided function. + * + * This method allows customization of the path generation by accepting a + * lambda function that takes a language string and returns the corresponding + * path as a string. + * + * @param path A lambda function that generates a path string when provided with a language code. + * @return The Builder instance with the updated path, enabling method chaining. + */ + fun path(path: (language: String) -> String) = apply { + this.path = path + } + + /** + * Configures the storage settings for the builder. + * + * This method allows the binding of a specific storage implementation + * with the builder configuration to manage data storage operations. + * + * @param storage The storage implementation of type `TolgeeStorageProvider`. + * This parameter defines how data will be stored and retrieved. + * @return The Builder instance with the configured storage, enabling method chaining. + */ + fun storage(storage: TolgeeStorageProvider) = apply { + this.storage = storage + } + /** * Sets the formatter to be used for formatting translation messages. * @@ -590,6 +677,8 @@ open class Tolgee( */ fun build(): ContentDelivery = ContentDelivery( url = url?.ifBlank { null }, + path = path, + storage = storage, formatter = formatter ) } @@ -690,86 +779,72 @@ open class Tolgee( * The instance is accessed lazily and is nullable, meaning it may return `null` if * the `Tolgee` instance is not initialized. This is the primary entry point for * interacting with the `Tolgee` localization and translation functionalities. - * - * Use this property to retrieve the currently active `Tolgee` instance, or initialize - * a new instance if required via supporting functions. */ @JvmStatic - val instance: PlatformTolgee? + val instanceOrNull: PlatformTolgee? get() = _instance.value /** - * Initializes the Tolgee framework with the specified configuration and optionally sets it as the global instance. + * Provides the singleton instance of the `Tolgee` class. + * + * Throws an `IllegalStateException` if the instance has not been initialized. + */ + @JvmStatic + val instance: PlatformTolgee + get() = _instance.value ?: throw IllegalStateException("Tolgee instance not initialized") + + /** + * Initializes the Tolgee framework with the specified configuration and sets it as the global instance. * - * @param global A boolean flag indicating whether the initialized instance should be set as the global instance. - * Defaults to true if the current global instance is null. * @param config The configuration object used to initialize the Tolgee instance. */ @JvmStatic @JvmOverloads fun init( - global: Boolean = _instance.value == null, config: Config - ) = createPlatformTolgee(config).also { - if (global) { - _instance.value = it + ) { + if (_instance.value != null) { + throw IllegalStateException("Tolgee is already initialized!") + } + val tolgee = createPlatformTolgee(config) + if (!_instance.compareAndSet(null, tolgee)) { + throw IllegalStateException("Tolgee is already initialized!") } } /** - * Initializes the Tolgee instance with the provided configuration options. - * - * This method sets up the configuration for the Tolgee library and prepares it for use. - * Configuration options are specified via a [Config.Builder] block, allowing - * customization of different settings such as API URL, authentication, and other - * Tolgee features. + * Initializes the Tolgee instance with the provided configuration options and sets it as the global instance. * - * @param global A flag indicating whether to initialize this instance globally. Defaults - * to true if no instance has been initialized yet; otherwise, false. * @param builder A lambda function used to configure the builder for creating the Tolgee configuration. */ @JvmStatic @JvmOverloads fun init( - global: Boolean = _instance.value == null, builder: Config.Builder.() -> Unit - ) = init(global, Config.Builder().apply(builder).build()) + ) { + init(Config.Builder().apply(builder).build()) + } /** - * Returns the existing instance of Tolgee if it has been initialized; otherwise, initializes - * a new instance using the provided configuration and optional global context. + * Initializes the Tolgee framework with the specified configuration and returns it. * - * This method ensures that the Tolgee instance is either reused if it already exists, - * or created and configured if it does not. It relies on a lazy initialization strategy. - * - * @param global Specifies whether the instance should be initialized in a global context. - * Defaults to `true` if no instance currently exists; otherwise, `false`. - * @param config The configuration object that provides the necessary setup details for - * initializing the Tolgee instance. + * @param config The configuration object used to initialize the Tolgee instance. */ @JvmStatic @JvmOverloads - fun instanceOrInit( - global: Boolean = _instance.value == null, + fun new( config: Config - ) = instance ?: init(global, config) + ) = createPlatformTolgee(config) /** - * Retrieves the current instance of the `Tolgee` class if it exists; otherwise, initializes a new instance. + * Initializes the Tolgee instance with the provided configuration options and returns it. * - * This method checks if the global instance of the `Tolgee` class is already initialized. If not, - * it initializes a new instance using the provided configuration builder. - * The method ensures thread-safe initialization and allows customization of the `Tolgee` - * configuration through the builder parameter. - * - * @param global A Boolean flag determining whether the instance should be initialized globally. Defaults to true if no instance exists. - * @param builder A lambda function for building the `Config` used during initialization. + * @param builder A lambda function used to configure the builder for creating the Tolgee configuration. */ @JvmStatic @JvmOverloads - fun instanceOrInit( - global: Boolean = _instance.value == null, + fun new( builder: Config.Builder.() -> Unit - ) = instance ?: init(global, builder) + ) = new(Config.Builder().apply(builder).build()) } } diff --git a/core/src/commonMain/kotlin/io/tolgee/api/TolgeeApi.kt b/core/src/commonMain/kotlin/io/tolgee/api/TolgeeApi.kt index 337bb793..bf8b0e4c 100644 --- a/core/src/commonMain/kotlin/io/tolgee/api/TolgeeApi.kt +++ b/core/src/commonMain/kotlin/io/tolgee/api/TolgeeApi.kt @@ -7,9 +7,9 @@ import io.ktor.client.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* +import io.ktor.utils.io.core.toByteArray import io.tolgee.Tolgee import io.tolgee.common.keyData -import io.tolgee.common.stringValue import io.tolgee.model.TolgeeKey import io.tolgee.model.TolgeeTranslation import io.tolgee.model.translation.TranslationEmpty @@ -39,13 +39,9 @@ internal data object TolgeeApi { } /** - * Retrieves translations using the Tolgee API or falls back to a CDN if necessary. + * Retrieves translations from the CDN. * - * This method fetches translations from the Tolgee server with pagination support. - * If the API key is not provided or translations are not available, it defaults to - * fetching translations from the CDN. - * - * @param client The HTTP client used to make requests to the Tolgee API. + * @param client The HTTP client used to make requests to the CDN. * @param config The configuration object containing API URL, project ID, and API key. * @param currentLanguage The language to fetch translations for. This is optional. * @return A [TolgeeTranslation] object containing the retrieved translations. @@ -55,7 +51,51 @@ internal data object TolgeeApi { config: Tolgee.Config, currentLanguage: String? ): TolgeeTranslation { - return getTranslationFromCDN(client, config, currentLanguage) + val storage = config.contentDelivery.storage + val language = currentLanguage?.ifBlank { null } + ?: config.locale?.language?.ifBlank { null } + ?: Tolgee.systemLocale.language.ifBlank { null } + ?: return TranslationEmpty + val path = config.contentDelivery.path(language) + + val fresh = getTranslationFromCDN(client, config, path) + val decoded = fresh?.decodeTranslation(config, language) + + if (decoded != null) { + storage?.put(path, fresh.toByteArray()) + return decoded + } + + val cached = storage?.get(path)?.decodeToString()?.decodeTranslation(config, language) + return cached ?: TranslationEmpty + } + + /** + * Decodes a localization file's content in JSON format into a `TolgeeTranslation` object. + * + * @param config The configuration object used for content delivery and formatting. + * @param language The language code for the language being processed. + * @return A `TolgeeTranslation` object containing parsed keys and translations, or `null` + * if the decoding process fails or the input is invalid. + */ + private suspend fun String.decodeTranslation( + config: Tolgee.Config, + language: String, + ): TolgeeTranslation? { + val decoded = suspendCatching { + json.decodeFromString>(this@decodeTranslation) + }.getOrNull() ?: return null + + return TolgeeTranslation( + keys = decoded.map { (key, value) -> + TolgeeKey( + keyName = key, + translations = mapOf(language to value.keyData()) + ) + }.toImmutableList(), + formatter = config.contentDelivery.formatter, + usedLocale = forLocaleTag(language), + ) } /** @@ -66,43 +106,26 @@ internal data object TolgeeApi { * * @param client The HTTP client used to perform the network request. * @param config The Tolgee configuration object containing CDN-related settings and fallback locale. - * @param currentLanguage The current language code for which translations are being retrieved. If null, fallback mechanisms are applied. + * @param path The path to the translation file within the CDN url. * @return A [TolgeeTranslation] object containing the retrieved translations, or a fallback empty translation object if no valid translations are available. */ suspend fun getTranslationFromCDN( client: HttpClient, config: Tolgee.Config, - currentLanguage: String? - ): TolgeeTranslation { - val baseUrl = config.contentDelivery.url?.ifBlank { null } ?: return TranslationEmpty - val language = currentLanguage?.ifBlank { null } - ?: config.locale?.language?.ifBlank { null } - ?: Tolgee.systemLocale.language.ifBlank { null } - ?: return TranslationEmpty + path: String + ): String? { + val baseUrl = config.contentDelivery.url?.ifBlank { null } ?: return null val start = if (baseUrl.endsWith('/')) baseUrl else "$baseUrl/" - val response = client.get("$start$language.json") { + val response = client.get("$start$path") { headers { append("sdkType", Tolgee.TYPE_HEADER) append("sdkVersion", Tolgee.VERSION_HEADER) } }.takeIf { it.status.isSuccess() - } ?: return TranslationEmpty - - val decoded = suspendCatching { - json.decodeFromString>(response.readRawBytes().decodeToString()) - }.getOrNull() ?: return TranslationEmpty + } ?: return null - return TolgeeTranslation( - keys = decoded.map { (key, value) -> - TolgeeKey( - keyName = key, - translations = mapOf(language to value.keyData()) - ) - }.toImmutableList(), - formatter = config.contentDelivery.formatter, - usedLocale = forLocaleTag(language), - ) + return response.readRawBytes().decodeToString() } } diff --git a/core/src/commonMain/kotlin/io/tolgee/common/ExtendCoroutines.kt b/core/src/commonMain/kotlin/io/tolgee/common/ExtendCoroutines.kt deleted file mode 100644 index 63473909..00000000 --- a/core/src/commonMain/kotlin/io/tolgee/common/ExtendCoroutines.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.tolgee.common - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.mapNotNull - -/** - * Transforms the elements emitted by this flow, filtering out any null values. - * - * This operator skips any `null` values emitted by the original flow and emits only the non-null values. - * - * @return A new flow that emits only non-null values from the original flow. - */ -fun Flow.mapNotNull(): Flow = this.mapNotNull { value -> value } \ No newline at end of file diff --git a/core/src/commonMain/kotlin/io/tolgee/common/ExtendParser.kt b/core/src/commonMain/kotlin/io/tolgee/common/ExtendParser.kt index 00eec222..25923b43 100644 --- a/core/src/commonMain/kotlin/io/tolgee/common/ExtendParser.kt +++ b/core/src/commonMain/kotlin/io/tolgee/common/ExtendParser.kt @@ -20,6 +20,7 @@ internal fun JsonElement.stringValue(): String = when (this) { internal fun JsonElement.keyData(): TolgeeKey.Data = when (this) { is JsonArray -> TolgeeKey.Data.Array(map { it.stringValue() }) + is JsonObject -> TolgeeKey.Data.Plural(mapValues { it.value.stringValue() }) else -> TolgeeKey.Data.Text(stringValue()) } diff --git a/core/src/commonMain/kotlin/io/tolgee/common/ExtendTolgee.kt b/core/src/commonMain/kotlin/io/tolgee/common/ExtendTolgee.kt index a6b7e0b1..208b9199 100644 --- a/core/src/commonMain/kotlin/io/tolgee/common/ExtendTolgee.kt +++ b/core/src/commonMain/kotlin/io/tolgee/common/ExtendTolgee.kt @@ -2,6 +2,7 @@ package io.tolgee.common import io.tolgee.Tolgee import io.ktor.client.* +import io.tolgee.storage.TolgeeStorageProvider import kotlin.coroutines.CoroutineContext /** @@ -31,6 +32,14 @@ internal expect val platformHttpClient: HttpClient */ internal expect val platformNetworkContext: CoroutineContext +/** + * Expected declaration for the platform-specific storage implementation. + * + * This property is expected to be provided by each platform's implementation + * to enable storage-related functionalities. + */ +internal expect val platformStorage: TolgeeStorageProvider? + /** * Platform-specific implementation of the Tolgee interface. * This class is expected to provide platform-dependent functionalities diff --git a/core/src/commonMain/kotlin/io/tolgee/model/TolgeeKey.kt b/core/src/commonMain/kotlin/io/tolgee/model/TolgeeKey.kt index 4d8f33d2..cd2a5d38 100644 --- a/core/src/commonMain/kotlin/io/tolgee/model/TolgeeKey.kt +++ b/core/src/commonMain/kotlin/io/tolgee/model/TolgeeKey.kt @@ -21,6 +21,8 @@ internal data class TolgeeKey( ) { val isText = translations.values.all { it is Data.Text } + val isPlural = translations.values.any { it is Data.Plural } + val isArray = translations.values.any { it is Data.Array } /** * Retrieves a translation for a given language code or the first available non-null translation. @@ -38,6 +40,9 @@ internal data class TolgeeKey( @Serializable data class Text(val text: String) : Data + @Serializable + data class Plural(val plurals: Map) : Data + @Serializable data class Array(val array: List) : Data } diff --git a/core/src/commonMain/kotlin/io/tolgee/model/TolgeePagedResponse.kt b/core/src/commonMain/kotlin/io/tolgee/model/TolgeePagedResponse.kt deleted file mode 100644 index 54409873..00000000 --- a/core/src/commonMain/kotlin/io/tolgee/model/TolgeePagedResponse.kt +++ /dev/null @@ -1,75 +0,0 @@ -package io.tolgee.model - -import kotlinx.serialization.Contextual -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** - * Represents a paginated response structure used by the Tolgee platform. - * This data class encapsulates the necessary attributes for handling - * paginated data and associated metadata, such as pagination details, - * links, and language selection. - * - * @param T The type of the embedded data contained within the paginated response. - * @property embedded The main content or data object embedded in the paginated response. - * @property links An optional object containing navigation links related to the current response. - * @property page An optional object with information about the paging state, like page size and total elements. - * @property selectedLanguages An optional set of selected languages applicable to the current response. - * @property nextCursor An optional string that represents the cursor for the next set of paginated data. - */ -@Serializable -internal data class TolgeePagedResponse( - @SerialName("_embedded") @Contextual val embedded: T, - @SerialName("_links") val links: Links? = null, - @SerialName("page") val page: Page? = null, - @SerialName("selectedLanguages") val selectedLanguages: Set? = null, - @SerialName("nextCursor") val nextCursor: String? = null, -) { - - /** - * Represents a collection of hypermedia links related to a resource. - * - * This data class defines a structure for handling HATEOAS-compliant links, - * allowing for navigation and resource discovery in APIs. - * - * @property self The link to the resource itself, encapsulated in a [Link] object. - */ - @Serializable - data class Links( - @SerialName("self") val self: Link? = null, - ) { - - /** - * Represents a hyperlink with a specified target URL. - * - * The `Link` data class is primarily used to encapsulate a single hyperlink, including its - * destination (href). It is typically employed in contexts where hyperlinks need to be - * serialized or deserialized, such as within the API responses or data transfer objects. - * - * @property href The URL to which the link points. - */ - @Serializable - data class Link( - @SerialName("href") val href: String, - ) - } - - /** - * Represents pagination details for a paginated response. - * - * This class encapsulates information about the pagination status, such as the size of each page, - * the total number of elements and pages, and the current page number. - * - * @property size The number of elements on a single page. - * @property totalElements The total number of elements across all pages. - * @property totalPages The total number of pages available. - * @property number The current page number (zero-based index). - */ - @Serializable - data class Page( - @SerialName("size") val size: Int, - @SerialName("totalElements") val totalElements: Int, - @SerialName("totalPages") val totalPages: Int, - @SerialName("number") val number: Int, - ) -} diff --git a/core/src/commonMain/kotlin/io/tolgee/model/TolgeeProjectLanguage.kt b/core/src/commonMain/kotlin/io/tolgee/model/TolgeeProjectLanguage.kt deleted file mode 100644 index 78022dad..00000000 --- a/core/src/commonMain/kotlin/io/tolgee/model/TolgeeProjectLanguage.kt +++ /dev/null @@ -1,52 +0,0 @@ -package io.tolgee.model - -import de.comahe.i18n4k.forLocaleTag -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -/** - * Represents a language configuration within a Tolgee project. - * - * This data class defines properties and functionality for managing a language associated - * with a specific Tolgee project. Each instance includes details such as the language's name, - * tag, original name, flag emoji, and whether it serves as the base language. - * - * @property name The name of the language (e.g., "English"). - * @property tag The language's unique tag identifier, typically in the format of a locale (e.g., "en-US"). - * @property originalName The original name of the language in its native form, if available. - * @property flagEmoji The emoji representing the country's or language's flag, if available. - * @property base Indicates whether this language is the project's base language. - */ -@Serializable -@ConsistentCopyVisibility -data class TolgeeProjectLanguage internal constructor( - @SerialName("name") val name: String, - @SerialName("tag") val tag: String, - @SerialName("originalName") val originalName: String? = null, - @SerialName("flagEmoji") val flagEmoji: String? = null, - @SerialName("base") val base: Boolean -) { - - /** - * Converts the associated language tag of this `TolgeeProjectLanguage` instance into a `Locale`. - * - * This function utilizes the `tag` property of the `TolgeeProjectLanguage` to generate a `Locale` - * object that corresponds to the specified tag. This is useful for working with localization and - * internationalization features where locale-specific data is required. - * - * @return A `Locale` object corresponding to the language tag of this instance. - */ - fun asLocale() = forLocaleTag(tag) - - /** - * Represents a paginated wrapper for a list of Tolgee project languages. - * This class is used to encapsulate a list of languages in a paged response structure. - * - * @property languages A list of `TolgeeProjectLanguage` objects, each representing a language - * used in a Tolgee project, including its metadata and related attributes. - */ - @Serializable - internal data class PagedWrapper( - @SerialName("languages") val languages: List - ) -} diff --git a/core/src/commonMain/kotlin/io/tolgee/model/translation/TranslationICU.kt b/core/src/commonMain/kotlin/io/tolgee/model/translation/TranslationICU.kt index 4fe28d23..e8306626 100644 --- a/core/src/commonMain/kotlin/io/tolgee/model/translation/TranslationICU.kt +++ b/core/src/commonMain/kotlin/io/tolgee/model/translation/TranslationICU.kt @@ -47,7 +47,7 @@ internal data class TranslationICU( }.groupBy({ it.first }, { it.second }) @Transient - private val stringArrayKeys = keys.filter { !it.isText } + private val stringArrayKeys = keys.filter { it.isArray } /** * A collection of message providers grouped by locale. Each provider implements the [MessagesProvider] interface. diff --git a/core/src/commonMain/kotlin/io/tolgee/model/translation/TranslationSprintf.kt b/core/src/commonMain/kotlin/io/tolgee/model/translation/TranslationSprintf.kt index 5a9fa4e9..ef85b7c4 100644 --- a/core/src/commonMain/kotlin/io/tolgee/model/translation/TranslationSprintf.kt +++ b/core/src/commonMain/kotlin/io/tolgee/model/translation/TranslationSprintf.kt @@ -1,6 +1,10 @@ package io.tolgee.model.translation import de.comahe.i18n4k.Locale +import de.comahe.i18n4k.cldr.plurals.PluralCategory +import de.comahe.i18n4k.cldr.plurals.PluralRule +import de.comahe.i18n4k.cldr.plurals.PluralRuleType +import de.comahe.i18n4k.createLocale import de.comahe.i18n4k.language import io.tolgee.common.sprintf import io.tolgee.model.TolgeeKey @@ -24,7 +28,7 @@ internal data class TranslationSprintf( private var usedLocale: Locale?, ) : TolgeeTranslation { - private val stringArrayKeys = keys.filter { !it.isText } + private val stringArrayKeys = keys.filter { it.isArray } /** * Retrieves a localized string for the specified key and formatting parameters, considering a given locale. @@ -52,10 +56,29 @@ internal data class TranslationSprintf( ?: this.usedLocale?.language?.ifBlank { null } )) { is TolgeeKey.Data.Text -> return data.text.sprintf(*args) + is TolgeeKey.Data.Plural -> { + val selected = data.plurals[getPluralName(args.getOrNull(0))] + return selected?.sprintf(*args) + } else -> null } } + fun getPluralName(number: Any?): String { + if (number == null) return PluralCategory.OTHER.id + val locale = usedLocale ?: return PluralCategory.OTHER.id + + val pluralRule = PluralRule.create(createLocale(locale.language), PluralRuleType.CARDINAL) + + val pluralCategory = when (number) { + is Number -> pluralRule?.select(number) + is String -> pluralRule?.select(number) + else -> PluralCategory.OTHER + } ?: PluralCategory.OTHER + + return pluralCategory.id + } + /** * Checks if the specified locale matches the currently used locale or has the same language as the used locale. * @@ -70,6 +93,7 @@ internal data class TranslationSprintf( val foundTolgeeKey = stringArrayKeys.firstOrNull { it.keyName == key } ?: return emptyList() return when (val data = foundTolgeeKey.translationForOrFirst(locale?.language)) { is TolgeeKey.Data.Array -> data.array + is TolgeeKey.Data.Plural -> data.plurals.map { it.value } is TolgeeKey.Data.Text -> listOf(data.text) else -> emptyList() } diff --git a/core/src/commonMain/kotlin/io/tolgee/storage/TolgeeStorageProvider.kt b/core/src/commonMain/kotlin/io/tolgee/storage/TolgeeStorageProvider.kt new file mode 100644 index 00000000..9e808599 --- /dev/null +++ b/core/src/commonMain/kotlin/io/tolgee/storage/TolgeeStorageProvider.kt @@ -0,0 +1,6 @@ +package io.tolgee.storage + +interface TolgeeStorageProvider { + fun put(name: String, data: ByteArray) + fun get(name: String): ByteArray? +} \ No newline at end of file diff --git a/core/src/jsMain/kotlin/io/tolgee/common/ExtendTolgee.js.kt b/core/src/jsMain/kotlin/io/tolgee/common/ExtendTolgee.js.kt index 170bb8f1..ec9bf3d3 100644 --- a/core/src/jsMain/kotlin/io/tolgee/common/ExtendTolgee.js.kt +++ b/core/src/jsMain/kotlin/io/tolgee/common/ExtendTolgee.js.kt @@ -4,6 +4,7 @@ import io.ktor.client.* import io.ktor.client.engine.js.* import io.ktor.client.plugins.cache.* import io.tolgee.Tolgee +import io.tolgee.storage.TolgeeStorageProvider import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext @@ -47,6 +48,9 @@ internal actual val platformHttpClient: HttpClient = HttpClient(Js) { internal actual val platformNetworkContext: CoroutineContext get() = Dispatchers.Default +internal actual val platformStorage: TolgeeStorageProvider? + get() = null + /** * A platform-specific implementation of the `Tolgee` class. * diff --git a/core/src/jvmMain/kotlin/io/tolgee/common/ExtendTolgee.jvm.kt b/core/src/jvmMain/kotlin/io/tolgee/common/ExtendTolgee.jvm.kt index 7b4d33a1..67bdb281 100644 --- a/core/src/jvmMain/kotlin/io/tolgee/common/ExtendTolgee.jvm.kt +++ b/core/src/jvmMain/kotlin/io/tolgee/common/ExtendTolgee.jvm.kt @@ -4,6 +4,7 @@ import io.tolgee.Tolgee import io.ktor.client.* import io.ktor.client.engine.java.Java import io.ktor.client.plugins.cache.* +import io.tolgee.storage.TolgeeStorageProvider import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext @@ -44,6 +45,9 @@ internal actual val platformHttpClient: HttpClient = HttpClient(Java) { internal actual val platformNetworkContext: CoroutineContext get() = Dispatchers.IO +internal actual val platformStorage: TolgeeStorageProvider? + get() = null + /** * Platform-specific implementation of the Tolgee class. * diff --git a/core/src/linuxMain/kotlin/io/tolgee/common/ExtendTolgee.linux.kt b/core/src/linuxMain/kotlin/io/tolgee/common/ExtendTolgee.linux.kt index b0ef5e4a..06027844 100644 --- a/core/src/linuxMain/kotlin/io/tolgee/common/ExtendTolgee.linux.kt +++ b/core/src/linuxMain/kotlin/io/tolgee/common/ExtendTolgee.linux.kt @@ -4,6 +4,7 @@ import io.tolgee.Tolgee import io.ktor.client.* import io.ktor.client.engine.curl.Curl import io.ktor.client.plugins.cache.* +import io.tolgee.storage.TolgeeStorageProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlin.coroutines.CoroutineContext @@ -45,6 +46,9 @@ internal actual val platformHttpClient: HttpClient = HttpClient(Curl) { internal actual val platformNetworkContext: CoroutineContext get() = Dispatchers.IO +internal actual val platformStorage: TolgeeStorageProvider? + get() = null + /** * An actual implementation of the `Tolgee` class for a specific platform. * This class is used to handle platform-specific operations and configurations diff --git a/core/src/mingwMain/kotlin/io/tolgee/common/ExtendTolgee.mingw.kt b/core/src/mingwMain/kotlin/io/tolgee/common/ExtendTolgee.mingw.kt index ec6a0d18..5f43d2c6 100644 --- a/core/src/mingwMain/kotlin/io/tolgee/common/ExtendTolgee.mingw.kt +++ b/core/src/mingwMain/kotlin/io/tolgee/common/ExtendTolgee.mingw.kt @@ -4,6 +4,7 @@ import io.tolgee.Tolgee import io.ktor.client.* import io.ktor.client.engine.winhttp.* import io.ktor.client.plugins.cache.* +import io.tolgee.storage.TolgeeStorageProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlin.coroutines.CoroutineContext @@ -42,6 +43,9 @@ internal actual val platformHttpClient: HttpClient = HttpClient(WinHttp) { internal actual val platformNetworkContext: CoroutineContext get() = Dispatchers.IO +internal actual val platformStorage: TolgeeStorageProvider? + get() = null + /** * Actual implementation of the `PlatformTolgee` class for a specific platform. * It extends the `Tolgee` base class and is initialized with a `Config` object. diff --git a/core/src/wasmJsMain/kotlin/io/tolgee/common/ExtendTolgee.wasmJs.kt b/core/src/wasmJsMain/kotlin/io/tolgee/common/ExtendTolgee.wasmJs.kt index 21c40eef..cc66db47 100644 --- a/core/src/wasmJsMain/kotlin/io/tolgee/common/ExtendTolgee.wasmJs.kt +++ b/core/src/wasmJsMain/kotlin/io/tolgee/common/ExtendTolgee.wasmJs.kt @@ -4,6 +4,7 @@ import io.tolgee.Tolgee import io.ktor.client.* import io.ktor.client.engine.js.* import io.ktor.client.plugins.cache.* +import io.tolgee.storage.TolgeeStorageProvider import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext @@ -41,6 +42,9 @@ internal actual val platformHttpClient: HttpClient = HttpClient(Js) { internal actual val platformNetworkContext: CoroutineContext get() = Dispatchers.Default +internal actual val platformStorage: TolgeeStorageProvider? + get() = null + /** * A platform-specific implementation of the Tolgee class. * This class extends the Tolgee base class and provides platform-dependent behavior. diff --git a/demo/android-view/build.gradle.kts b/demo/android-view/build.gradle.kts deleted file mode 100644 index 6cc27dfa..00000000 --- a/demo/android-view/build.gradle.kts +++ /dev/null @@ -1,30 +0,0 @@ -plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.android) -} - -android { - namespace = "io.tolgee.demo.view" - compileSdk = 35 - - defaultConfig { - applicationId = "io.tolgee.demo.view" - minSdk = 21 - targetSdk = 35 - versionCode = 1 - versionName = "1.0" - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_21 - } -} - -dependencies { - implementation(libs.android) - implementation(libs.activity) - - implementation(libs.coroutines.android) - implementation(project(":core")) -} \ No newline at end of file diff --git a/demo/android-view/src/main/AndroidManifest.xml b/demo/android-view/src/main/AndroidManifest.xml deleted file mode 100644 index c4660b70..00000000 --- a/demo/android-view/src/main/AndroidManifest.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/demo/android-view/src/main/ic_launcher-playstore.png b/demo/android-view/src/main/ic_launcher-playstore.png deleted file mode 100644 index 586d2baa..00000000 Binary files a/demo/android-view/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/demo/android-view/src/main/kotlin/io/tolgee/demo/view/MainActivity.kt b/demo/android-view/src/main/kotlin/io/tolgee/demo/view/MainActivity.kt deleted file mode 100644 index 70480da1..00000000 --- a/demo/android-view/src/main/kotlin/io/tolgee/demo/view/MainActivity.kt +++ /dev/null @@ -1,83 +0,0 @@ -package io.tolgee.demo.view - -import android.content.Context -import android.os.Bundle -import android.widget.Button -import android.widget.TextView -import androidx.activity.ComponentActivity -import androidx.lifecycle.lifecycleScope -import io.tolgee.Tolgee -import io.tolgee.TolgeeContextWrapper -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.util.* - -class MainActivity : ComponentActivity() { - - val tolgee = Tolgee.instanceOrInit { - - } - - override fun attachBaseContext(newBase: Context?) { - super.attachBaseContext(TolgeeContextWrapper.wrap(newBase)) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContentView(R.layout.activity_main) - - val basic = findViewById(R.id.basic_text) - val parameter = findViewById(R.id.parameterized_text) - val button = findViewById