diff --git a/.claude/commands/release.md b/.claude/commands/release.md new file mode 100644 index 0000000..3c99014 --- /dev/null +++ b/.claude/commands/release.md @@ -0,0 +1,109 @@ +--- +allowed-tools: Bash(grep:*), Bash(git describe:*), Bash(git branch:*), Bash(git log:*), Bash(git diff:*), Bash(git status:*), Bash(git tag:*), Bash(git add:*), Bash(git commit:*), Bash(git reset:*), Bash(gh repo view:*), Bash(gh release view:*), Bash(gh run:*), Bash(gh run list:*), Bash(gh run watch:*), Bash(gh run rerun:*), Bash(gh workflow run:*), Bash(./gradlew:*), Edit, Read +description: Release a new version - bump version, commit, tag, push, publish to Maven Central, create GitHub release +argument-hint: "[major|minor|patch|]" +--- + +# Release + +## Current State + +- Current version: !`grep '^library' gradle/libs.versions.toml` +- Latest git tag: !`git describe --tags --abbrev=0 2>/dev/null || echo "(no tags)"` +- Current branch: !`git branch --show-current` +- GitHub repo: !`gh repo view --json nameWithOwner -q .nameWithOwner` + +## Instructions + +Follow these steps precisely to create a new release. + +### Step 1: Preflight checks + +1. Verify we are on the `master` branch. If not, **stop** and warn the user. +2. Verify the working tree is clean (`git status --porcelain`). If not, **stop** and warn the user. +3. Pull latest: `git pull --ff-only`. If this fails, **stop** and warn the user. + +### Step 2: Determine version bump + +The current version uses pre-release identifiers (e.g., `1.0.0-alpha03`). Version bumping rules: + +If `$ARGUMENTS` is one of `major`, `minor`, or `patch`: +- **patch**: increment the last numeric segment (e.g., `1.0.0-alpha03` → `1.0.0-alpha04`, or `1.2.3` → `1.2.4`) +- **minor**: `X.Y.Z` → `X.Y+1.0` (drops any pre-release suffix) +- **major**: `X.Y.Z` → `X+1.0.0` (drops any pre-release suffix) — always confirm with user first + +If `$ARGUMENTS` is an explicit version string (e.g., `1.0.0-beta01` or `1.0.0`), use it directly. + +Otherwise, analyze the commits since the last tag and suggest an appropriate bump. Present the proposed new version to the user and ask them to confirm using `AskUserQuestion`. + +### Step 3: Summarize changes + +List the commits since the last tag using `git log ..HEAD --oneline`. Write concise, user-facing bullet points for each meaningful change. These will be used in the GitHub release body alongside the auto-generated changelog. + +### Step 4: Execute the release + +Do all of the following in order: + +1. Edit `gradle/libs.versions.toml` to update the `library` version on line 9 to the new version +2. Build to verify nothing is broken: + ``` + ./gradlew apiDump + ./gradlew :core:build + ``` + If the build fails, **stop** and help the user fix the issue before continuing. +3. Stage and commit: + ``` + git add gradle/libs.versions.toml core/api/ + git commit -m "release: vX.Y.Z" + ``` +4. Tag: `git tag vX.Y.Z` +5. Push: `git push && git push --tags` + +### Step 5: Wait for CI + +After pushing, monitor the CI workflows triggered by the push: + +1. Wait a moment for workflows to trigger, then use `gh run list --branch master --limit 5` to find the runs +2. Use `gh run watch ` on the test workflow run + +If all checks pass, proceed to Step 6. + +If any check fails: +- **Flaky/random failure**: Rerun with `gh run rerun --failed`, then wait again +- **Real failure**: We need to roll back and fix the issue: + 1. Delete the remote tag: `git push --delete origin vX.Y.Z` + 2. Delete the local tag: `git tag -d vX.Y.Z` + 3. Revert the release commit: `git reset --hard HEAD~1 && git push --force` + 4. Help the user fix the issue, then re-release + +### Step 6: Create GitHub release + +Create the release on GitHub using the auto-generated changelog: + +``` +gh release create vX.Y.Z --title "vX.Y.Z" --generate-notes +``` + +This will: +- Create a GitHub release with auto-generated PR-based changelog +- Automatically trigger the `apple-binaries.yml` workflow (attaches XCFramework archives to the release) + +### Step 7: Trigger Maven Central publish + +Trigger the publish workflow manually: + +``` +gh workflow run publish.yml +``` + +### Step 8: Monitor post-release workflows + +1. Use `gh run list --limit 5` to find the publish and apple-binaries workflow runs +2. Watch both with `gh run watch ` +3. If the publish workflow fails, inform the user — they may need to check Maven Central / Sonatype credentials +4. If apple-binaries fails, it can be re-triggered: `gh workflow run apple-binaries.yml` + +Once all workflows complete successfully, report: +- Link to the GitHub release +- Confirm Maven Central publish status +- Confirm Apple XCFramework archives attached to release diff --git a/.junie/guidelines.md b/.junie/guidelines.md deleted file mode 100644 index ad20187..0000000 --- a/.junie/guidelines.md +++ /dev/null @@ -1,158 +0,0 @@ -# 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/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..66401c7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,119 @@ +# AGENTS.md + +Guidance for AI coding agents working on the Tolgee Mobile Kotlin SDK. + +Main branch: `master` + +## Project Structure + +``` +tolgee-mobile-kotlin-sdk/ +├── core/ # KMP base library (translations, caching, CDN API) +├── compose/ # Compose Multiplatform integration +├── compiler-plugin/ # Kotlin compiler plugin (currently disabled) +├── gradle-plugin/ # Gradle plugin for project integration +└── demo/ # Example apps (Android Views, Jetpack Compose, KMP Compose) +``` + +The compiler plugin is commented out in `settings.gradle.kts` (broken after Kotlin 2.2). + +## Building + +```bash +./gradlew :core:build +./gradlew :compose:build +./gradlew :gradle-plugin:build +``` + +## Testing + +Only the gradle-plugin module has tests currently: + +```bash +./gradlew :gradle-plugin:test +./gradlew :gradle-plugin:test --tests "TolgeeTest" +``` + +CI (`test.yml`) runs gradle-plugin tests on every push. Requires Tolgee CLI (`npm install --global @tolgee/cli`). + +## API Compatibility + +The project uses the Binary Compatibility Validator plugin. After any changes to public APIs, run: + +```bash +./gradlew apiDump +``` + +This updates `.api` dump files in each module's `api/` directory. These files **must be committed** with the change. The build will fail (`apiCheck`) if the dump is out of date. + +## Module Architecture + +### Core Module + +Main entry point: `Tolgee` singleton class in `core/src/commonMain/kotlin/io/tolgee/Tolgee.kt` + +Key components: +- `Tolgee` — Singleton with locale management, translation resolution, and configuration +- `TolgeeApi` — CDN communication and local caching +- `TolgeeTranslation` — Interface with ICU and sprintf formatting implementations +- `TolgeeStorageProvider` — Platform-specific persistent caching interface +- `TolgeeManifest` — Available locales metadata from CDN (internal) + +Data flow: +``` +tFlow("key", params) + → localeFlow emits locale + → loadManifest() fetches available locales from CDN + → resolveLocale() applies progressive BCP 47 fallback (zh-Hans-CN → zh-Hans → zh) + → loadTranslations() fetches from CDN or cache (LRU in-memory + persistent storage) + → Translation formatted and emitted via Flow +``` + +Thread safety: `Mutex` for translation loading, `AtomicFU` for manifest cache. + +### Compose Module + +Composable wrappers around Core with graceful fallback to default Compose resources when Tolgee is not initialized. Provides `stringResource()`, `pluralStringResource()`, `stringArrayResource()`. + +### Gradle Plugin + +Build-time configuration DSL bridging Gradle with the compiler plugin. Configures replacement of `getString()` and `stringResource()` calls. + +### Compiler Plugin (disabled) + +Kotlin IR transformations to replace standard resource calls with Tolgee calls at compile-time. Currently disabled in `settings.gradle.kts`. + +## Multiplatform + +The core module targets 20+ platforms: + +| Category | Targets | +|----------|---------| +| Android | androidTarget, androidNativeX64/X86/Arm64/Arm32 | +| JVM | jvm (toolchain 21) | +| Apple | iOS (x64, arm64, simulatorArm64), tvOS (×3), watchOS (×4), macOS (x64, arm64) | +| Other | linuxX64, linuxArm64, mingwX64, js (IR), wasmJs | + +Source set hierarchy follows `applyDefaultHierarchyTemplate()`: +- `commonMain` — All shared code +- `androidMain` — Android-specific (Views integration, storage) +- `appleMain` — Apple platforms (iOS/macOS/tvOS/watchOS) +- `jvmMain` — JVM-specific +- `jsMain` / `wasmJsMain` — JS/WASM + +## Key Design Patterns + +- **Builder pattern** for configuration: `Tolgee.Config.Builder`, `ContentDelivery.Builder`, `Network.Builder` +- **Sealed interfaces** for type safety: `Formatter` (ICU/Sprintf), `TolgeeMessageParams` (None/Indexed/Mapped) +- **Reactive Flows**: `localeFlow`, `changeFlow`, `tFlow()` returning `Flow` +- **Expect/actual** for platform-specific implementations (`platformHttpClient`, `platformStorage`, etc.) + +## Dependencies + +Managed via Version Catalog in `gradle/libs.versions.toml`. Add new dependencies there, not directly in build scripts. + +Key libraries: Ktor (HTTP), kotlinx-serialization (JSON), kotlinx-coroutines (async), i18n4k (locale handling), AtomicFU (thread safety), SKIE (Swift interop for Apple targets). + +## Publishing + +Manual trigger via `publish.yml` workflow on macOS. Publishes to Maven Central via `publishAllPublicationsToMavenCentralRepository`. Requires signing keys and Sonatype credentials. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/core/api/android/core.api b/core/api/android/core.api index c5db8f6..f3ba2e0 100644 --- a/core/api/android/core.api +++ b/core/api/android/core.api @@ -2,6 +2,8 @@ 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 fun getAvailableLocaleTags ()Ljava/util/List; + public fun getAvailableLocales ()Ljava/util/List; public final fun getChangeFlow ()Lkotlinx/coroutines/flow/MutableSharedFlow; public fun getConfig ()Lio/tolgee/Tolgee$Config; public static final fun getInstance ()Lio/tolgee/TolgeeAndroid; diff --git a/core/api/jvm/core.api b/core/api/jvm/core.api index 7d0c1ab..3181477 100644 --- a/core/api/jvm/core.api +++ b/core/api/jvm/core.api @@ -2,6 +2,8 @@ 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 fun getAvailableLocaleTags ()Ljava/util/List; + public fun getAvailableLocales ()Ljava/util/List; public final fun getChangeFlow ()Lkotlinx/coroutines/flow/MutableSharedFlow; public fun getConfig ()Lio/tolgee/Tolgee$Config; public static final fun getInstance ()Lio/tolgee/common/PlatformTolgee; diff --git a/core/src/commonMain/kotlin/io/tolgee/Tolgee.kt b/core/src/commonMain/kotlin/io/tolgee/Tolgee.kt index e9cc29d..b53bc6d 100644 --- a/core/src/commonMain/kotlin/io/tolgee/Tolgee.kt +++ b/core/src/commonMain/kotlin/io/tolgee/Tolgee.kt @@ -496,6 +496,37 @@ open class Tolgee( */ open fun getLocale() = localeFlow.value ?: systemLocale + /** + * The list of available locales from the manifest or configuration. + * + * Returns the locales configured via [Config.Builder.availableLocales], or if not + * manually configured, the locales fetched from the CDN manifest. + * + * Returns null if the manifest hasn't been loaded yet and no manual config was provided. + * Call [preload] first to ensure the manifest is loaded. + * + * For reactive updates, combine with [changeFlow]: + * ```kotlin + * tolgee.changeFlow.collect { + * val locales = tolgee.availableLocales + * } + * ``` + */ + open val availableLocales: List? + get() = config.availableLocales ?: cachedManifest.value?.availableLocales + + /** + * The list of available locale tags (e.g., `["en", "fr", "de"]`) from the manifest or configuration. + * + * Returns null if the manifest hasn't been loaded yet and no manual config was provided. + * Call [preload] first to ensure the manifest is loaded. + * + * @see availableLocales + */ + open val availableLocaleTags: List? + get() = config.availableLocales?.map { it.toTag("-") } + ?: cachedManifest.value?.locales + /** * Represents the configuration used for API integration and content management. *