Skip to content

feat(details): Implement translation for "About" and "What's New" sec…#277

Merged
rainxchzed merged 5 commits intomainfrom
feat-translate
Mar 1, 2026
Merged

feat(details): Implement translation for "About" and "What's New" sec…#277
rainxchzed merged 5 commits intomainfrom
feat-translate

Conversation

@rainxchzed
Copy link
Member

@rainxchzed rainxchzed commented Mar 1, 2026

…tions

This commit introduces a translation feature leveraging the Google Translate API to allow users to translate repository descriptions (README) and release notes within the app. It includes a language picker and state management for toggling between original and translated content.

  • feat(details): Added TranslationRepository and its implementation using Ktor to interface with the Google Translate API, including text chunking for large documents and a simple LRU cache.
  • feat(details): Introduced LanguagePicker and TranslationControls UI components to manage language selection and translation states.
  • feat(details): Updated DetailsViewModel and DetailsState to handle translation logic, including support for automatic device language detection.
  • feat(details): Integrated translation controls into the About and WhatsNew UI sections.
  • domain: Added SupportedLanguage, TranslationResult, and TranslationState models.
  • i18n: Added translation-related string resources for multiple languages (English, Bengali, Spanish, French, Hindi, Italian, Japanese, Korean, Polish, Russian, Turkish, and Chinese).
  • chore(di): Registered TranslationRepository in the Koin detailsModule.

Summary by CodeRabbit

  • New Features

    • In-app translation: translate content, language picker, toggle original/translated view, translation status and error messaging
    • Support for many localized translation labels across locales
    • Clipboard link detection with banner, detected-links UI, and FAB for quick GitHub link actions
    • Direct navigation to repository details from detected links
    • Profile setting to auto-detect clipboard links (toggle persists)
  • UI

    • Language list and translation controls added to About and What's New sections

…tions

This commit introduces a translation feature leveraging the Google Translate API to allow users to translate repository descriptions (README) and release notes within the app. It includes a language picker and state management for toggling between original and translated content.

- **feat(details)**: Added `TranslationRepository` and its implementation using Ktor to interface with the Google Translate API, including text chunking for large documents and a simple LRU cache.
- **feat(details)**: Introduced `LanguagePicker` and `TranslationControls` UI components to manage language selection and translation states.
- **feat(details)**: Updated `DetailsViewModel` and `DetailsState` to handle translation logic, including support for automatic device language detection.
- **feat(details)**: Integrated translation controls into the `About` and `WhatsNew` UI sections.
- **domain**: Added `SupportedLanguage`, `TranslationResult`, and `TranslationState` models.
- **i18n**: Added translation-related string resources for multiple languages (English, Bengali, Spanish, French, Hindi, Italian, Japanese, Korean, Polish, Russian, Turkish, and Chinese).
- **chore(di)**: Registered `TranslationRepository` in the Koin `detailsModule`.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

Walkthrough

Adds a translation feature (domain, data, presentation) with UI controls and language picker, wires a TranslationRepository implementation (HTTP + caching), expands localization strings, adds clipboard GitHub-link detection/navigation, and exposes an auto-detect-clipboard preference across theme/profile layers.

Changes

Cohort / File(s) Summary
Localization resources
core/presentation/src/commonMain/composeResources/values/strings.xml, core/presentation/src/commonMain/composeResources/values-*/strings-*.xml
Added translation-related strings across base and many locale files; added GitHub-clipboard related strings in base strings.xml.
Translation domain & models
feature/details/domain/src/commonMain/kotlin/.../TranslationResult.kt, .../SupportedLanguage.kt, .../repository/TranslationRepository.kt
New data models and TranslationRepository interface (translate + getDeviceLanguageCode).
Translation data layer & DI
feature/details/data/src/commonMain/kotlin/.../TranslationRepositoryImpl.kt, .../di/SharedModule.kt
New TranslationRepositoryImpl implementing chunked Google Translate calls, JSON parsing, LRU-like cache, close(); DI binding added.
Presentation models & languages
feature/details/presentation/src/.../model/TranslationState.kt, .../model/SupportedLanguages.kt
Added TranslationState UI model and curated SupportedLanguages list.
UI components (Details)
feature/details/presentation/src/.../components/LanguagePicker.kt, .../TranslationControls.kt, components/sections/About.kt, components/sections/WhatsNew.kt, DetailsRoot.kt
New LanguagePicker and TranslationControls; about/whatsNew updated to accept translation state/callbacks; DetailsRoot wired to language picker and translation flows.
ViewModel & actions (Details)
feature/details/presentation/src/.../DetailsAction.kt, DetailsState.kt, DetailsViewModel.kt
New translation actions and state fields; DetailsViewModel injected with TranslationRepository and handles translation lifecycle, device language init, and error handling.
Search: clipboard & link parsing
feature/search/presentation/src/.../SearchRoot.kt, SearchViewModel.kt, SearchState.kt, utils/GithubUrlParser.kt, SearchAction.kt, SearchEvent.kt
Added GitHub URL parser, clipboard-detected links state/UI, FAB/clipboard handling, navigation event for owner/repo, and new SearchRoot navigation callback.
Navigation
composeApp/src/commonMain/kotlin/.../AppNavigation.kt, composeApp/src/.../GithubStoreGraph.kt
Added onNavigateToDetailsFromLink callback and DetailsScreen param isComingFromUpdate; updated navigation routes to support owner/repo params.
Clipboard helpers & domain
core/domain/src/commonMain/.../utils/ClipboardHelper.kt, core/data/src/androidMain/.../AndroidClipboardHelper.kt, core/data/src/jvmMain/.../DesktopClipboardHelper.kt
Added getText(): String? to ClipboardHelper and platform implementations to read clipboard text safely.
Themes / profile: auto-detect clipboard setting
core/data/.../ThemesRepositoryImpl.kt, core/domain/.../repository/ThemesRepository.kt, feature/profile/presentation/.../ProfileState.kt, ProfileAction.kt, ProfileViewModel.kt, Appearance.kt
Added preference key and get/set methods for auto-detect clipboard links; profile state, action, ViewModel wiring, and UI toggle added.
Minor view/signature updates
feature/details/presentation/.../components/*, feature/details/presentation/.../model/*
Adjusted many composable function signatures to accept translation parameters and updated presentation flows to render translated content.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as Details UI
    participant VM as DetailsViewModel
    participant Repo as TranslationRepositoryImpl
    participant API as Google Translate API
    participant Cache as In-memory Cache

    User->>UI: request translation / pick language
    UI->>VM: TranslateAbout/TranslateWhatsNew(targetLang)
    VM->>Cache: lookup(key=hash(text)+targetLang)
    alt cache hit
        Cache-->>VM: cached TranslationResult
    else cache miss
        VM->>Repo: translate(text, targetLang)
        Repo->>Repo: chunk text
        loop for each chunk
            Repo->>API: POST chunk
            API-->>Repo: JSON response
        end
        Repo->>Repo: parse & join chunks
        Repo->>Cache: store(key, TranslationResult)
        Repo-->>VM: TranslationResult
    end
    VM->>VM: update TranslationState
    VM-->>UI: new state (translatedText / detected language)
    UI->>User: render translated content / controls
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I nibble words and hop through code,

I split long lines on my little road,
A picker pops — choose tongue and cheer,
Cached translations saved so near,
I hop away with languages owed.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature: implementing translation for About and What's New sections in the details view.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-translate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt (1)

128-140: ⚠️ Potential issue | 🟡 Minor

Minor visual concern: content height resets on translation toggle.

The contentHeightPx state is keyed on displayContent, so it resets to 0f whenever translation is toggled. This causes needsExpansion to briefly be false until the content is re-measured, potentially causing a flicker where the expand/collapse button momentarily disappears.

Consider preserving the measured height or using a separate key that doesn't reset on content changes:

💡 Suggested approach
-var contentHeightPx by remember(displayContent, collapsedHeightPx) {
+var contentHeightPx by remember(collapsedHeightPx) {
     mutableFloatStateOf(0f)
 }

This allows the height to accumulate correctly as content changes, since onGloballyPositioned only updates if the new measurement is larger.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt`
around lines 128 - 140, The measured content height (contentHeightPx) is being
recreated whenever displayContent changes, causing a reset to 0f and a flicker
when toggling translation; update the remember for contentHeightPx so it is not
keyed on displayContent (e.g., key only on collapsedHeightPx or no key) and
change the onGloballyPositioned update to only set contentHeightPx when the new
measured height is greater than the current value (so measurements accumulate
instead of resetting), keeping needsExpansion computed from contentHeightPx and
collapsedHeightPx as before.
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt (1)

143-145: ⚠️ Potential issue | 🟡 Minor

Same height reset concern as in WhatsNew.kt.

The contentHeightPx state keyed on content will reset to 0f when toggling between original and translated content, potentially causing brief visual flicker. Consider the same fix as suggested for WhatsNew.kt.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt`
around lines 143 - 145, The contentHeightPx state is being reset because it's
remembered with content as a key; change the remember call so contentHeightPx is
not reset when toggling original/translated content (e.g. replace
remember(content, collapsedHeightPx) { mutableStateOf(0f) } with
remember(collapsedHeightPx) { mutableStateOf(0f) } or simply remember {
mutableStateOf(0f) } and update it when collapsedHeightPx changes), keeping
collapsedHeightPx calculation the same; ensure needsExpansion logic
(contentHeightPx > collapsedHeightPx && collapsedHeightPx > 0f) continues to
work without flicker.
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt (1)

1070-1087: ⚠️ Potential issue | 🟠 Major

viewModelScope.launch in onCleared() will not execute reliably.

When onCleared() is called, viewModelScope is already cancelled. Launching a coroutine—even with NonCancellable—on a cancelled scope will fail to execute. The cleanup work will not run as intended.

Consider using GlobalScope or a separate CoroutineScope for cleanup that must outlive the ViewModel:

🐛 Proposed fix
+import kotlinx.coroutines.GlobalScope

 override fun onCleared() {
     super.onCleared()
     currentDownloadJob?.cancel()

     val assetsToClean = listOfNotNull(currentAssetName, cachedDownloadAssetName).distinct()
     if (assetsToClean.isNotEmpty()) {
-        viewModelScope.launch(NonCancellable) {
+        GlobalScope.launch {
             for (asset in assetsToClean) {
                 try {
                     downloader.cancelDownload(asset)
                     logger.debug("Cleaned up download on screen leave: $asset")
                 } catch (t: Throwable) {
                     logger.error("Failed to clean download on leave: ${t.message}")
                 }
             }
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt`
around lines 1070 - 1087, onCleared currently calls
viewModelScope.launch(NonCancellable) which won't run because viewModelScope is
already cancelled; fix by creating and using a dedicated cleanup scope (e.g.,
add a property cleanupScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
on the ViewModel) and replace viewModelScope.launch(NonCancellable) with
cleanupScope.launch { ... } so the loop invoking
downloader.cancelDownload(asset) and logger.* runs reliably; reference the
existing onCleared, currentDownloadJob, currentAssetName,
cachedDownloadAssetName, downloader.cancelDownload and logger to locate and
update the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt`:
- Around line 35-36: The cacheKey construction in TranslationRepositoryImpl
currently uses text.hashCode(), which can collide; update the key used for the
cache map (the cacheKey variable) to uniquely identify the text + targetLanguage
by including the text itself (or a sufficiently long truncated prefix) or by
using a collision-resistant digest (e.g., SHA-256) of text, so replace the
hashCode-based key with a text-aware or cryptographic-hash-based key when
creating cacheKey before reading/writing cache.
- Around line 26-27: The LinkedHashMap cache (cache) in
TranslationRepositoryImpl is not thread-safe and must be protected for
concurrent translate() calls; wrap every access (reads, writes, eviction checks
that use maxCacheSize) in a synchronized(cache) block or replace with a
concurrent-safe structure and ensure eviction logic runs atomically. Locate all
references to cache and maxCacheSize (particularly inside the translate() method
and any put/get/contains operations) and modify them so reads and writes occur
inside synchronized(cache) { ... } blocks (or switch to a thread-safe LRU
implementation) so the cache cannot be concurrently mutated and size trimming is
atomic.
- Line 22: TranslationRepositoryImpl currently constructs a private HttpClient
(httpClient) at initialization and never closes it, risking resource leaks;
either make TranslationRepositoryImpl implement Closeable/AutoCloseable and add
a close() method that calls httpClient.close(), or change the constructor to
accept an injected HttpClient (replace the createPlatformHttpClient call) so the
caller manages its lifecycle; update usages/creations of
TranslationRepositoryImpl accordingly to close or reuse the shared client.
- Around line 72-80: The code in TranslationRepositoryImpl that uses
httpClient.get to call "translate.googleapis.com/translate_a/single" (producing
responseText) must be replaced with calls to the official Google Cloud
Translation API (v2 or v3); update the method that constructs the request (where
httpClient.get and responseText are used) to use either the Google Cloud client
library or the documented REST endpoint, implement proper authentication (API
key for v2 or OAuth/service account for v3), build the request
payload/parameters per the chosen API (e.g., q, source, target for v2 or the v3
translateText JSON body), parse the official response shape instead of the
unofficial responseText, and add error handling for quota/429 and retries;
ensure to update any references to response parsing logic and tests that depend
on the old unofficial response format.

---

Outside diff comments:
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt`:
- Around line 143-145: The contentHeightPx state is being reset because it's
remembered with content as a key; change the remember call so contentHeightPx is
not reset when toggling original/translated content (e.g. replace
remember(content, collapsedHeightPx) { mutableStateOf(0f) } with
remember(collapsedHeightPx) { mutableStateOf(0f) } or simply remember {
mutableStateOf(0f) } and update it when collapsedHeightPx changes), keeping
collapsedHeightPx calculation the same; ensure needsExpansion logic
(contentHeightPx > collapsedHeightPx && collapsedHeightPx > 0f) continues to
work without flicker.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt`:
- Around line 128-140: The measured content height (contentHeightPx) is being
recreated whenever displayContent changes, causing a reset to 0f and a flicker
when toggling translation; update the remember for contentHeightPx so it is not
keyed on displayContent (e.g., key only on collapsedHeightPx or no key) and
change the onGloballyPositioned update to only set contentHeightPx when the new
measured height is greater than the current value (so measurements accumulate
instead of resetting), keeping needsExpansion computed from contentHeightPx and
collapsedHeightPx as before.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt`:
- Around line 1070-1087: onCleared currently calls
viewModelScope.launch(NonCancellable) which won't run because viewModelScope is
already cancelled; fix by creating and using a dedicated cleanup scope (e.g.,
add a property cleanupScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
on the ViewModel) and replace viewModelScope.launch(NonCancellable) with
cleanupScope.launch { ... } so the loop invoking
downloader.cancelDownload(asset) and logger.* runs reliably; reference the
existing onCleared, currentDownloadJob, currentAssetName,
cachedDownloadAssetName, downloader.cancelDownload and logger to locate and
update the code.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e419a7 and 2ea3c63.

📒 Files selected for processing (27)
  • core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml
  • core/presentation/src/commonMain/composeResources/values-es/strings-es.xml
  • core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml
  • core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml
  • core/presentation/src/commonMain/composeResources/values-it/strings-it.xml
  • core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml
  • core/presentation/src/commonMain/composeResources/values-kr/strings-kr.xml
  • core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml
  • core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml
  • core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml
  • core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt
  • feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/SupportedLanguage.kt
  • feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/TranslationResult.kt
  • feature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/repository/TranslationRepository.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationControls.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/model/SupportedLanguages.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/model/TranslationState.kt

Comment on lines +35 to +36
val cacheKey = "${text.hashCode()}:$targetLanguage"
cache[cacheKey]?.let { return it }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Cache key using hashCode() risks collisions between different texts.

Different strings can produce the same hash code, leading to incorrect cache hits where one translation is returned for a different text.

Consider using a more collision-resistant key, or include the actual text (potentially truncated) to differentiate:

💡 Suggested fix
-val cacheKey = "${text.hashCode()}:$targetLanguage"
+val cacheKey = "${text.take(100)}:${text.length}:${text.hashCode()}:$targetLanguage"

Or use a cryptographic hash if text privacy is a concern:

val cacheKey = "${text.sha256()}:$targetLanguage"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val cacheKey = "${text.hashCode()}:$targetLanguage"
cache[cacheKey]?.let { return it }
val cacheKey = "${text.take(100)}:${text.length}:${text.hashCode()}:$targetLanguage"
cache[cacheKey]?.let { return it }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt`
around lines 35 - 36, The cacheKey construction in TranslationRepositoryImpl
currently uses text.hashCode(), which can collide; update the key used for the
cache map (the cacheKey variable) to uniquely identify the text + targetLanguage
by including the text itself (or a sufficiently long truncated prefix) or by
using a collision-resistant digest (e.g., SHA-256) of text, so replace the
hashCode-based key with a text-aware or cryptographic-hash-based key when
creating cacheKey before reading/writing cache.

This commit introduces the ability to detect GitHub repository links within the search query and from the system clipboard. Users can now quickly navigate to repository details by pasting or typing GitHub URLs, and the app will provide a banner for auto-detected links from the clipboard.

- **feat(search)**: Added `GithubUrlParser` to identify and extract owner/repo metadata from GitHub URLs.
- **feat(search)**: Implemented a clipboard banner that appears when a GitHub link is detected in the system clipboard.
- **feat(search)**: Added a Floating Action Button (FAB) to manually trigger GitHub link detection from the clipboard.
- **feat(search)**: Enhanced the search logic to automatically suggest navigation when the query consists entirely of GitHub URLs.
- **feat(profile)**: Added a setting in the Appearance section to enable or disable automatic clipboard link detection.
- **feat(core)**: Extended `ClipboardHelper` with `getText()` support for Android and Desktop platforms.
- **refactor(core)**: Updated `ThemesRepository` to manage the persistence of the clipboard detection preference.
- **i18n**: Added string resources for clipboard detection, link labels, and app-specific navigation prompts.
…TranslationRepositoryImpl`

This commit enhances the `TranslationRepositoryImpl` by introducing thread-safe cache access using a Mutex and ensuring proper disposal of the HTTP client.

- **refactor(details)**: Implemented `AutoCloseable` in `TranslationRepositoryImpl` to properly close the `httpClient`.
- **refactor(details)**: Added `Mutex` to synchronize access to the translation cache, preventing potential race conditions.
- **refactor(details)**: Removed unused `GitHubStoreLogger` dependency from `TranslationRepositoryImpl`.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt (1)

19-23: Consider registering an onClose callback to release resources.

TranslationRepositoryImpl implements AutoCloseable but Koin's single scope won't automatically call close() when the module is unloaded. For long-running processes or proper resource cleanup, consider adding an onClose callback.

💡 Suggested improvement
     single<TranslationRepository> {
         TranslationRepositoryImpl(
             localizationManager = get()
         )
-    }
+    } onClose { (it as? AutoCloseable)?.close() }

As per coding guidelines: "Use Koin for dependency injection in SharedModule.kt to define detailsModule."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt`
around lines 19 - 23, The TranslationRepositoryImpl registered as a singleton
implements AutoCloseable but is not being closed; update the Koin registration
in SharedModule.kt where single<TranslationRepository> is declared to add an
onClose callback that invokes close() on the TranslationRepositoryImpl instance
(or casts to AutoCloseable) so resources are released when the module is
unloaded or Koin stops.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ThemesRepositoryImpl.kt`:
- Around line 78-81: The current implementation of getAutoDetectClipboardLinks
in ThemesRepositoryImpl defaults AUTO_DETECT_CLIPBOARD_KEY to true which enables
clipboard reads by default; change the fallback to false so
prefs[AUTO_DETECT_CLIPBOARD_KEY] ?: false to make clipboard auto-detect opt‑in,
update any related documentation/tests that assume default true, and ensure
getAutoDetectClipboardLinks continues to return Flow<Boolean> with the new
default.

In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt`:
- Around line 88-106: The parseTranslationResponse function currently assumes a
fixed JSON array structure and directly indexes root[0] and
segment.jsonArray[0], risking IndexOutOfBoundsException; update
parseTranslationResponse to defensively validate the parsed JsonElement shape
(check that json.parseToJsonElement(responseText) is a JsonArray, that
root.getOrNull(0) and root[0].jsonArray are present, and that each segment has a
jsonArray with a first element) before accessing, use safe helpers like
getOrNull and jsonArrayOrNull/jsonPrimitiveOrNull (or wrap the parsing block in
a try-catch) and provide a safe fallback TranslationResult (e.g., empty
translatedText and null detectedSourceLanguage) when the structure is unexpected
so TranslationResult construction never throws.
- Around line 43-52: The code ignores the original chunk delimiters returned by
chunkText() and always rejoins translated chunks with "\n\n", breaking original
formatting; update the loop that iterates "for ((chunkText, _) in chunks)" to
capture the delimiter (e.g., "for ((chunkText, delimiter) in chunks)"), collect
translated text together with its corresponding delimiter (instead of only
adding response.translatedText to translatedChunks), and build the final
translatedText by concatenating each translated chunk followed by its stored
delimiter (preserving original spacing) before creating the TranslationResult;
keep translateSingleChunk(...) and detectedLang handling the same.

In
`@feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt`:
- Around line 348-357: The early-return branch in SearchViewModel where
isEntirelyGithubUrls(action.query) is true updates query/detectedLinks but
leaves stale repositories and totalCount in _state; modify the _state.update
call in that branch (and the similar branch handling multi-link FAB) to also
reset repositories to an empty list, totalCount to 0 (and any paging flags like
isLastPage/isLoadingMore as appropriate) so link-only mode doesn't show previous
results; locate the update in SearchViewModel (the if using isEntirelyGithubUrls
and the second similar block mentioned) and include repositories = emptyList(),
totalCount = 0 (and clear any pagination state) in the copied state.

In
`@feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/GithubUrlParser.kt`:
- Around line 9-11: The regex GITHUB_URL_REGEX can match substrings like
"mygithub.com/owner/repo"; update GITHUB_URL_REGEX to require a host boundary
before "github.com" by inserting a negative lookbehind such as
(?<![A-Za-z0-9\-_.]) immediately before the existing (?:https?://)?(?:www\.)?
portion so that "github.com" is not preceded by alphanumeric/-,_ or .
characters; keep the existing capture groups for owner and repo unchanged.

---

Nitpick comments:
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt`:
- Around line 19-23: The TranslationRepositoryImpl registered as a singleton
implements AutoCloseable but is not being closed; update the Koin registration
in SharedModule.kt where single<TranslationRepository> is declared to add an
onClose callback that invokes close() on the TranslationRepositoryImpl instance
(or casts to AutoCloseable) so resources are released when the module is
unloaded or Koin stops.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ea3c63 and 4b8b909.

📒 Files selected for processing (21)
  • composeApp/release/baselineProfiles/0/composeApp-release.dm
  • composeApp/release/baselineProfiles/1/composeApp-release.dm
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt
  • core/data/src/androidMain/kotlin/zed/rainxch/core/data/utils/AndroidClipboardHelper.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ThemesRepositoryImpl.kt
  • core/data/src/jvmMain/kotlin/zed/rainxch/core/data/utils/DesktopClipboardHelper.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ThemesRepository.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/utils/ClipboardHelper.kt
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt
  • feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt
  • feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt
  • feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt
  • feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Appearance.kt
  • feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt
  • feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchEvent.kt
  • feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt
  • feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt
  • feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt
  • feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/GithubUrlParser.kt

Comment on lines +43 to +52
for ((chunkText, _) in chunks) {
val response = translateSingleChunk(chunkText, targetLanguage, sourceLanguage)
translatedChunks.add(response.translatedText)
if (detectedLang == null) {
detectedLang = response.detectedSourceLanguage
}
}

val result = TranslationResult(
translatedText = translatedChunks.joinToString("\n\n"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Chunk delimiters are collected but not used when reassembling translation.

The chunkText() method returns Pair<String, String> where the second element is the original delimiter ("\n\n" or "\n" or ""), but this delimiter is ignored—the code always joins with "\n\n". This can corrupt formatting when large paragraphs are chunked with single newlines or no delimiter.

🛠️ Proposed fix to use stored delimiters
-val chunks = chunkText(text)
-val translatedChunks = mutableListOf<String>()
+val chunks = chunkText(text)
+val translatedParts = mutableListOf<Pair<String, String>>()
 var detectedLang: String? = null

 for ((chunkText, _) in chunks) {
+for ((chunkText, delimiter) in chunks) {
     val response = translateSingleChunk(chunkText, targetLanguage, sourceLanguage)
-    translatedChunks.add(response.translatedText)
+    translatedParts.add(response.translatedText to delimiter)
     if (detectedLang == null) {
         detectedLang = response.detectedSourceLanguage
     }
 }

 val result = TranslationResult(
-    translatedText = translatedChunks.joinToString("\n\n"),
+    translatedText = translatedParts.dropLast(1)
+        .joinToString("") { (text, delim) -> text + delim } +
+        translatedParts.lastOrNull()?.first.orEmpty(),
     detectedSourceLanguage = detectedLang
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt`
around lines 43 - 52, The code ignores the original chunk delimiters returned by
chunkText() and always rejoins translated chunks with "\n\n", breaking original
formatting; update the loop that iterates "for ((chunkText, _) in chunks)" to
capture the delimiter (e.g., "for ((chunkText, delimiter) in chunks)"), collect
translated text together with its corresponding delimiter (instead of only
adding response.translatedText to translatedChunks), and build the final
translatedText by concatenating each translated chunk followed by its stored
delimiter (preserving original spacing) before creating the TranslationResult;
keep translateSingleChunk(...) and detectedLang handling the same.

Comment on lines +75 to +83
val responseText = httpClient.get(
"https://translate.googleapis.com/translate_a/single"
) {
parameter("client", "gtx")
parameter("sl", sourceLanguage)
parameter("tl", targetLanguage)
parameter("dt", "t")
parameter("q", text)
}.bodyAsText()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing error handling for network and API failures.

translateSingleChunk performs a network call without any error handling. Network failures, HTTP errors (4xx/5xx), or malformed responses will propagate as uncaught exceptions, potentially crashing the app or leaving translations in an inconsistent state.

🛠️ Proposed fix with error handling
 private suspend fun translateSingleChunk(
     text: String,
     targetLanguage: String,
     sourceLanguage: String
 ): TranslationResult {
+    return try {
         val responseText = httpClient.get(
             "https://translate.googleapis.com/translate_a/single"
         ) {
             parameter("client", "gtx")
             parameter("sl", sourceLanguage)
             parameter("tl", targetLanguage)
             parameter("dt", "t")
             parameter("q", text)
         }.bodyAsText()
 
-    return parseTranslationResponse(responseText)
+        parseTranslationResponse(responseText)
+    } catch (e: Exception) {
+        // Return original text on failure
+        TranslationResult(
+            translatedText = text,
+            detectedSourceLanguage = null
+        )
+    }
 }

Comment on lines +348 to +357
if (isEntirelyGithubUrls(action.query)) {
currentSearchJob?.cancel()
_state.update {
it.copy(
isLoading = false,
isLoadingMore = false,
errorMessage = null
)
}
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

URL-only flows keep stale repository results in state.

When input is entirely GitHub URLs (typed query or multi-link FAB path), you update query/detectedLinks but keep old repositories/totalCount. That leaves unrelated old results rendered alongside link-only mode.

🔧 Proposed change
 if (isEntirelyGithubUrls(action.query)) {
     currentSearchJob?.cancel()
     _state.update {
         it.copy(
             isLoading = false,
             isLoadingMore = false,
-            errorMessage = null
+            errorMessage = null,
+            repositories = emptyList(),
+            totalCount = null
         )
     }
     return
 }
                         } else {
                             _state.update {
                                 it.copy(
                                     query = clipText,
                                     detectedLinks = links,
+                                    repositories = emptyList(),
+                                    totalCount = null,
+                                    isLoading = false,
+                                    isLoadingMore = false,
+                                    errorMessage = null,
                                 )
                             }
                         }

Also applies to: 488-493

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt`
around lines 348 - 357, The early-return branch in SearchViewModel where
isEntirelyGithubUrls(action.query) is true updates query/detectedLinks but
leaves stale repositories and totalCount in _state; modify the _state.update
call in that branch (and the similar branch handling multi-link FAB) to also
reset repositories to an empty list, totalCount to 0 (and any paging flags like
isLastPage/isLoadingMore as appropriate) so link-only mode doesn't show previous
results; locate the update in SearchViewModel (the if using isEntirelyGithubUrls
and the second similar block mentioned) and include repositories = emptyList(),
totalCount = 0 (and clear any pagination state) in the copied state.

…pdates

This commit introduces a new `isComingFromUpdate` flag to the details screen to improve the user experience when checking for app updates. When this flag is active, the "What's New" (release notes) section is displayed above the "About" (README) section.

- **feat(details)**: Added `isComingFromUpdate` parameter to `DetailsViewModel` and `DetailsState`.
- **feat(ui)**: Reordered the display of `whatsNew` and `about` sections in `DetailsRoot` based on the `isComingFromUpdate` flag.
- **feat(navigation)**: Updated `GithubStoreGraph` and `AppNavigation` to pass the `isComingFromUpdate` flag when navigating from the updates screen.
- **refactor(details)**: Simplified `DetailsState` by removing the unused `isTrackable` computed property.
- **chore**: Updated Koin view model factory to include the new navigation parameter.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt (1)

77-80: Consider using sealed interface for consistency.

Per coding guidelines, sealed classes/interfaces should be used for type-safe navigation routes, actions, and events. While TranslationTarget is simple with only two values, using a sealed interface would align with the project's conventions for discriminated types.

♻️ Optional: Convert to sealed interface
-enum class TranslationTarget {
-    ABOUT, WHATS_NEW
-}
+sealed interface TranslationTarget {
+    data object About : TranslationTarget
+    data object WhatsNew : TranslationTarget
+}

This would require updating the when expressions to use TranslationTarget.About and TranslationTarget.WhatsNew.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt`
around lines 77 - 80, Replace the enum TranslationTarget with a sealed interface
(named TranslationTarget) and implement its two cases as singleton objects
(TranslationTarget.About and TranslationTarget.WhatsNew); then update all usages
and when expressions that switch on TranslationTarget to match the
sealed-interface singletons (e.g., when (target) { TranslationTarget.About ->
...; TranslationTarget.WhatsNew -> ... }) so the type becomes a discriminated
sealed type consistent with project conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt`:
- Around line 77-80: Replace the enum TranslationTarget with a sealed interface
(named TranslationTarget) and implement its two cases as singleton objects
(TranslationTarget.About and TranslationTarget.WhatsNew); then update all usages
and when expressions that switch on TranslationTarget to match the
sealed-interface singletons (e.g., when (target) { TranslationTarget.About ->
...; TranslationTarget.WhatsNew -> ... }) so the type becomes a discriminated
sealed type consistent with project conventions.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b8b909 and 0cc2e2a.

📒 Files selected for processing (5)
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt

…rove translation handling

This commit refines the translation logic and search behavior while fixing some minor issues in URL parsing and clipboard detection. Key changes include converting the `TranslationTarget` to a sealed interface for better type safety and improving how translated text chunks are joined to preserve delimiters.

- **refactor(details)**: Converted `TranslationTarget` from an `enum` to a `sealed interface`.
- **refactor(details)**: Updated `TranslationRepositoryImpl` to correctly preserve delimiters between translated text chunks and added a fallback for parsing failures.
- **fix(search)**: Hardened `GITHUB_URL_REGEX` with a negative lookbehind to prevent incorrect matches on prefixed strings.
- **fix(search)**: Updated `SearchViewModel` to clear repository results and reset loading states when the query is cleared or multiple links are detected in the clipboard.
- **fix(core)**: Changed the default value of `AUTO_DETECT_CLIPBOARD_KEY` from `true` to `false` in `ThemesRepositoryImpl`.
- **chore**: Updated `DetailsRoot` to reflect `TranslationTarget` naming changes (`About`/`WhatsNew`).
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt (1)

454-466: ⚠️ Potential issue | 🟠 Major

Cancel active/debounced searches before forcing cleared or link-only state.

Line 455 and Line 495 reset state but do not cancel currentSearchJob / searchDebounceJob. A late response can repopulate repositories after clear or after switching to multi-link mode.

🔧 Suggested fix
             SearchAction.OnClearClick -> {
+                currentSearchJob?.cancel()
+                searchDebounceJob?.cancel()
+                currentPage = 1
                 _state.update {
                     it.copy(
                         query = "",
                         repositories = emptyList(),
                         isLoading = false,
                         isLoadingMore = false,
                         errorMessage = null,
                         totalCount = null,
                         detectedLinks = emptyList(),
+                        hasMorePages = false,
                     )
                 }
             }
                         } else {
+                            currentSearchJob?.cancel()
+                            searchDebounceJob?.cancel()
+                            currentPage = 1
                             _state.update {
                                 it.copy(
                                     query = clipText,
                                     detectedLinks = links,
                                     repositories = emptyList(),
                                     totalCount = null,
+                                    hasMorePages = false,
                                     isLoading = false,
                                     isLoadingMore = false,
                                     errorMessage = null,
                                 )
                             }
                         }

Based on learnings: "SearchViewModel must manage search state, filter management, and pagination."

Also applies to: 495-505

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt`
around lines 454 - 466, Before clearing or switching to link-only state in
SearchViewModel, cancel any in-flight search jobs to prevent late results from
repopulating the state: call cancel() on currentSearchJob and searchDebounceJob
(or check for null and cancel) before you call _state.update(...) in the handler
for SearchAction.OnClearClick and the handler around the lines that reset state
for link-only/multi-link mode (the same reset block around
totalCount/detectedLinks). Ensure you also null out or recreate those Job
references after cancelling so future searches start fresh.
♻️ Duplicate comments (1)
feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt (1)

73-96: ⚠️ Potential issue | 🟠 Major

HTTP call is outside the try-catch block—network failures will propagate uncaught.

The try-catch at lines 88-95 only wraps parseTranslationResponse(), but the HTTP call at lines 78-86 remains unprotected. Network timeouts, connection failures, and HTTP errors (4xx/5xx) will throw exceptions that propagate up, potentially crashing the app or leaving the translation in an inconsistent state.

🛠️ Proposed fix to wrap entire function body
 private suspend fun translateSingleChunk(
     text: String,
     targetLanguage: String,
     sourceLanguage: String
 ): TranslationResult {
+    return try {
         val responseText = httpClient.get(
             "https://translate.googleapis.com/translate_a/single"
         ) {
             parameter("client", "gtx")
             parameter("sl", sourceLanguage)
             parameter("tl", targetLanguage)
             parameter("dt", "t")
             parameter("q", text)
         }.bodyAsText()
 
-    return try {
         parseTranslationResponse(responseText)
     } catch (_: Exception) {
         TranslationResult(
             translatedText = text,
             detectedSourceLanguage = null
         )
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt`
around lines 73 - 96, The HTTP call in translateSingleChunk is not covered by
the try-catch so network or HTTP errors can escape; wrap the entire operation
(the httpClient.get(...).bodyAsText() call and the
parseTranslationResponse(responseText) call) in a single try-catch, catching
Exception, and on error return the same fallback
TranslationResult(translatedText = text, detectedSourceLanguage = null);
reference translateSingleChunk, httpClient.get, bodyAsText, and
parseTranslationResponse when making the change.
🧹 Nitpick comments (1)
feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt (1)

263-341: Consider extracting duplicated section rendering logic.

The about() and whatsNew() calls are identical between the if and else branches—only the ordering differs. This could be simplified by extracting the rendering into helper functions or using a more data-driven approach.

♻️ Optional refactor to reduce duplication
// Define reusable lambdas for section rendering
val aboutSection: LazyListScope.() -> Unit = {
    state.readmeMarkdown?.let {
        about(
            readmeMarkdown = state.readmeMarkdown,
            readmeLanguage = state.readmeLanguage,
            isExpanded = state.isAboutExpanded,
            onToggleExpanded = { onAction(DetailsAction.ToggleAboutExpanded) },
            collapsedHeight = collapsedSectionHeight,
            translationState = state.aboutTranslation,
            onTranslateClick = { onAction(DetailsAction.TranslateAbout(state.deviceLanguageCode)) },
            onLanguagePickerClick = { onAction(DetailsAction.ShowLanguagePicker(TranslationTarget.About)) },
            onToggleTranslation = { onAction(DetailsAction.ToggleAboutTranslation) },
        )
    }
}

val whatsNewSection: LazyListScope.() -> Unit = {
    state.selectedRelease?.let { release ->
        whatsNew(
            release = release,
            isExpanded = state.isWhatsNewExpanded,
            onToggleExpanded = { onAction(DetailsAction.ToggleWhatsNewExpanded) },
            collapsedHeight = collapsedSectionHeight,
            translationState = state.whatsNewTranslation,
            onTranslateClick = { onAction(DetailsAction.TranslateWhatsNew(state.deviceLanguageCode)) },
            onLanguagePickerClick = { onAction(DetailsAction.ShowLanguagePicker(TranslationTarget.WhatsNew)) },
            onToggleTranslation = { onAction(DetailsAction.ToggleWhatsNewTranslation) },
        )
    }
}

// Then use them based on ordering
if (state.isComingFromUpdate) {
    whatsNewSection()
    aboutSection()
} else {
    aboutSection()
    whatsNewSection()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt`
around lines 263 - 341, Duplicate rendering of the about() and whatsNew()
sections—extract the repeated blocks into reusable lambdas (e.g., val
aboutSection: LazyListScope.() -> Unit and val whatsNewSection: LazyListScope.()
-> Unit) that capture state.readmeMarkdown, state.selectedRelease,
collapsedSectionHeight, state.aboutTranslation, state.whatsNewTranslation and
call the existing about() and whatsNew() helpers with the same onAction lambdas
(DetailsAction.ToggleAboutExpanded, DetailsAction.TranslateAbout,
DetailsAction.ShowLanguagePicker(TranslationTarget.About),
DetailsAction.ToggleAboutTranslation, and the equivalent WhatsNew actions).
Replace the duplicated if/else bodies with a simple conditional that calls
either whatsNewSection() then aboutSection() when state.isComingFromUpdate is
true, or aboutSection() then whatsNewSection() otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt`:
- Around line 78-79: The code currently performs a GET to the undocumented gtx
endpoint when building responseText in TranslationRepositoryImpl; replace this
call with requests to the official Google Cloud Translation API (v2 or v3) using
authenticated calls (API key for v2 or OAuth/service account for v3) or an
official client library, update the logic in the method that calls
httpClient.get(...) to construct the proper POST/GET to the Cloud Translation
endpoint, send required parameters (model/target/source/text) per the chosen
API, handle and parse the official response format instead of the gtx format,
and add retry/backoff and quota/error handling for 403/rate-limit errors.

In
`@feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt`:
- Around line 96-98: When collecting
themesRepository.getAutoDetectClipboardLinks() in SearchViewModel (the block
using _state.update { it.copy(autoDetectClipboardEnabled = enabled) }), ensure
that when enabled is false you also clear any existing clipboard UI state: set
clipboardLinks to empty and isClipboardBannerVisible to false in the same
_state.update call so previously detected links and the banner are removed;
apply the same change to the analogous collector later in the file (the other
block handling auto-detect clipboard links around the clipboard detection
subscription) so both places reset clipboardLinks and isClipboardBannerVisible
when auto-detect is turned off.

---

Outside diff comments:
In
`@feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt`:
- Around line 454-466: Before clearing or switching to link-only state in
SearchViewModel, cancel any in-flight search jobs to prevent late results from
repopulating the state: call cancel() on currentSearchJob and searchDebounceJob
(or check for null and cancel) before you call _state.update(...) in the handler
for SearchAction.OnClearClick and the handler around the lines that reset state
for link-only/multi-link mode (the same reset block around
totalCount/detectedLinks). Ensure you also null out or recreate those Job
references after cancelling so future searches start fresh.

---

Duplicate comments:
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt`:
- Around line 73-96: The HTTP call in translateSingleChunk is not covered by the
try-catch so network or HTTP errors can escape; wrap the entire operation (the
httpClient.get(...).bodyAsText() call and the
parseTranslationResponse(responseText) call) in a single try-catch, catching
Exception, and on error return the same fallback
TranslationResult(translatedText = text, detectedSourceLanguage = null);
reference translateSingleChunk, httpClient.get, bodyAsText, and
parseTranslationResponse when making the change.

---

Nitpick comments:
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt`:
- Around line 263-341: Duplicate rendering of the about() and whatsNew()
sections—extract the repeated blocks into reusable lambdas (e.g., val
aboutSection: LazyListScope.() -> Unit and val whatsNewSection: LazyListScope.()
-> Unit) that capture state.readmeMarkdown, state.selectedRelease,
collapsedSectionHeight, state.aboutTranslation, state.whatsNewTranslation and
call the existing about() and whatsNew() helpers with the same onAction lambdas
(DetailsAction.ToggleAboutExpanded, DetailsAction.TranslateAbout,
DetailsAction.ShowLanguagePicker(TranslationTarget.About),
DetailsAction.ToggleAboutTranslation, and the equivalent WhatsNew actions).
Replace the duplicated if/else bodies with a simple conditional that calls
either whatsNewSection() then aboutSection() when state.isComingFromUpdate is
true, or aboutSection() then whatsNewSection() otherwise.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0cc2e2a and 0f5ed34.

📒 Files selected for processing (6)
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ThemesRepositoryImpl.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt
  • feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt
  • feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt
  • feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/GithubUrlParser.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/GithubUrlParser.kt

Comment on lines +78 to +79
val responseText = httpClient.get(
"https://translate.googleapis.com/translate_a/single"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Google Translate API gtx endpoint translate.googleapis.com unofficial 2025 2026

💡 Result:

translate.googleapis.com/translate_a/single?client=gtx… (often called the “gtx endpoint”) is an unofficial/undocumented Google Translate web backend that many community libraries hit directly (typically with client=gtx) and that can return 403 / captchas / throttling without warning. Community docs/packages still reference it as working intermittently into Feb 2026, but emphasize it’s “touchy” and subject to breakage. [1], [2]

Key points (2025–2026):

  • Not the official Google Cloud Translation API. The supported, documented service on translate.googleapis.com is Cloud Translation (v3 / v3beta1), which uses authenticated Google Cloud endpoints and published request/response contracts. [3]
  • Reliability is not guaranteed. Community packages explicitly suggest switching to client=gtx when they hit 403 Forbidden, implying frequent blocking/rate-limiting on these unofficial calls. [2]
  • Potential Terms of Service risk. Google’s terms have long included restrictions against accessing services via automated means or via interfaces other than those provided/authorized by Google—an argument commonly cited for why scraping/unofficial endpoints are risky. [4]
  • If you need production stability, use the official API. Cloud Translation enforces quotas and returns 403 on quota/rate issues, but it’s documented and configurable (quotas, billing, auth), unlike the unofficial gtx endpoint. [5]

Practical takeaway: for experiments, the client=gtx endpoint may still “work” in 2026 but is inherently brittle; for anything production/commercial, use Google Cloud Translation v2/v3 instead. [3], [5]

References:
[1] Medium (Feb 2026) discussion of the unofficial endpoint and client=gtx usage (maxkleiner1.medium.com)
[2] google-translate-api-x npm package note about 403 and client=gtx (npmjs.com)
[3] Google Cloud Translation REST reference (service: translate.googleapis.com) (cloud.google.com)
[4] Google Terms of Service archive showing “don’t access by automated means / other than the interface provided” language (policies.google.com)
[5] Google Cloud Translation quotas/limits (403 behavior, quota concepts) (cloud.google.com)

Citations:


🌐 Web query:

Google Translate API official documentation alternatives translate.googleapis.com

💡 Result:

If you mean the official Google “Translate API”

Google’s supported product is Google Cloud Translation (Translation AI), not the unofficial web endpoint often seen in scripts.

  • Cloud Translation – Basic (v2) (official REST): POST https://translation.googleapis.com/language/translate/v2 (OAuth / service account or API key support depends on setup) [1]
  • Cloud Translation – Advanced (v3) (official REST): POST https://translation.googleapis.com/v3/projects/{project}/:translateText [2]
  • Service / discovery endpoint documentation: https://translate.googleapis.com (service endpoint for the API) [3]

If you were using https://translate.googleapis.com/translate_a/single?client=gtx..., that’s widely described as an unofficial/internal endpoint, not a supported public API and may break or violate terms [4].

Alternatives to Google Cloud Translation (with official docs)

  • DeepL API (REST translate endpoint): https://api.deepl.com / https://api-free.deepl.com [5]
  • Microsoft Azure AI Translator (Text translation) (cloud REST translation service) [6]
  • Amazon Translate (TranslateText API) [7]
  • LibreTranslate (open-source; self-host or managed; POST /translate) [8]

Sources:
[1] Google Cloud Translation Basic v2 “Translate text” doc (docs.cloud.google.com)
[2] Google Cloud Translation Advanced v3 “Translate text” doc (docs.cloud.google.com)
[3] Google Cloud Translation REST reference (service endpoint / discovery docs) (docs.cloud.google.com)
[4] Example discussion noting /translate_a/single is unofficial (b4x.com)
[5] DeepL API reference (Translate) (developers.deepl.com)
[6] Azure Text translation overview (learn.microsoft.com)
[7] Amazon Translate TranslateText API reference (docs.aws.amazon.com)
[8] LibreTranslate documentation (/translate) (docs.libretranslate.com)

Citations:


Use official Google Cloud Translation API instead of the unofficial gtx endpoint.

The endpoint translate.googleapis.com/translate_a/single?client=gtx is an internal, undocumented Google service that frequently returns 403 Forbidden responses and captcha challenges. Community libraries document ongoing blocking and throttling issues as recently as February 2026. Using this endpoint also potentially violates Google's Terms of Service, which restrict accessing services via automated means or unauthorized interfaces.

Use Google Cloud Translation (v2 or v3) instead, which provides:

  • Official, documented REST API
  • Guaranteed quotas and rate limiting (with proper error handling)
  • Authentication and billing controls
  • Production stability

Alternatively, consider other official translation services like DeepL, Azure Translator, or Amazon Translate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt`
around lines 78 - 79, The code currently performs a GET to the undocumented gtx
endpoint when building responseText in TranslationRepositoryImpl; replace this
call with requests to the official Google Cloud Translation API (v2 or v3) using
authenticated calls (API key for v2 or OAuth/service account for v3) or an
official client library, update the logic in the method that calls
httpClient.get(...) to construct the proper POST/GET to the Cloud Translation
endpoint, send required parameters (model/target/source/text) per the chosen
API, handle and parse the official response format instead of the gtx format,
and add retry/backoff and quota/error handling for 403/rate-limit errors.

Comment on lines +96 to +98
themesRepository.getAutoDetectClipboardLinks().collect { enabled ->
_state.update { it.copy(autoDetectClipboardEnabled = enabled) }
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Synchronize clipboard UI state when auto-detect is turned off.

Line 96 only updates autoDetectClipboardEnabled. If links were already detected, disabling auto-detect can still leave clipboardLinks and isClipboardBannerVisible populated.

🔧 Suggested fix
 private fun observeClipboardSetting() {
     viewModelScope.launch {
         themesRepository.getAutoDetectClipboardLinks().collect { enabled ->
-            _state.update { it.copy(autoDetectClipboardEnabled = enabled) }
+            _state.update { current ->
+                current.copy(
+                    autoDetectClipboardEnabled = enabled,
+                    clipboardLinks = if (enabled) current.clipboardLinks else emptyList(),
+                    isClipboardBannerVisible = if (enabled) current.isClipboardBannerVisible else false
+                )
+            }
+            if (enabled) checkClipboardForLinks()
         }
     }
 }

Also applies to: 111-116

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt`
around lines 96 - 98, When collecting
themesRepository.getAutoDetectClipboardLinks() in SearchViewModel (the block
using _state.update { it.copy(autoDetectClipboardEnabled = enabled) }), ensure
that when enabled is false you also clear any existing clipboard UI state: set
clipboardLinks to empty and isClipboardBannerVisible to false in the same
_state.update call so previously detected links and the banner are removed;
apply the same change to the analogous collector later in the file (the other
block handling auto-detect clipboard links around the clipboard detection
subscription) so both places reset clipboardLinks and isClipboardBannerVisible
when auto-detect is turned off.

@rainxchzed rainxchzed mentioned this pull request Mar 1, 2026
@rainxchzed rainxchzed merged commit fb61bbd into main Mar 1, 2026
2 checks passed
@rainxchzed rainxchzed deleted the feat-translate branch March 1, 2026 10:14
@rainxchzed rainxchzed restored the feat-translate branch March 1, 2026 10:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant