feat(details): Implement translation for "About" and "What's New" sec…#277
feat(details): Implement translation for "About" and "What's New" sec…#277rainxchzed merged 5 commits intomainfrom
Conversation
…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`.
WalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟡 MinorMinor visual concern: content height resets on translation toggle.
The
contentHeightPxstate is keyed ondisplayContent, so it resets to0fwhenever translation is toggled. This causesneedsExpansionto briefly befalseuntil 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
onGloballyPositionedonly 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 | 🟡 MinorSame height reset concern as in WhatsNew.kt.
The
contentHeightPxstate keyed oncontentwill reset to0fwhen 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.launchinonCleared()will not execute reliably.When
onCleared()is called,viewModelScopeis already cancelled. Launching a coroutine—even withNonCancellable—on a cancelled scope will fail to execute. The cleanup work will not run as intended.Consider using
GlobalScopeor a separateCoroutineScopefor 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
📒 Files selected for processing (27)
core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xmlcore/presentation/src/commonMain/composeResources/values-es/strings-es.xmlcore/presentation/src/commonMain/composeResources/values-fr/strings-fr.xmlcore/presentation/src/commonMain/composeResources/values-hi/strings-hi.xmlcore/presentation/src/commonMain/composeResources/values-it/strings-it.xmlcore/presentation/src/commonMain/composeResources/values-ja/strings-ja.xmlcore/presentation/src/commonMain/composeResources/values-kr/strings-kr.xmlcore/presentation/src/commonMain/composeResources/values-pl/strings-pl.xmlcore/presentation/src/commonMain/composeResources/values-ru/strings-ru.xmlcore/presentation/src/commonMain/composeResources/values-tr/strings-tr.xmlcore/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xmlcore/presentation/src/commonMain/composeResources/values/strings.xmlfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.ktfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.ktfeature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/SupportedLanguage.ktfeature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/model/TranslationResult.ktfeature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/repository/TranslationRepository.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/LanguagePicker.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/TranslationControls.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/model/SupportedLanguages.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/model/TranslationState.kt
.../data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt
Show resolved
Hide resolved
.../data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt
Show resolved
Hide resolved
| val cacheKey = "${text.hashCode()}:$targetLanguage" | ||
| cache[cacheKey]?.let { return it } |
There was a problem hiding this comment.
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.
| 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`.
There was a problem hiding this comment.
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 anonClosecallback to release resources.
TranslationRepositoryImplimplementsAutoCloseablebut Koin'ssinglescope won't automatically callclose()when the module is unloaded. For long-running processes or proper resource cleanup, consider adding anonClosecallback.💡 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
📒 Files selected for processing (21)
composeApp/release/baselineProfiles/0/composeApp-release.dmcomposeApp/release/baselineProfiles/1/composeApp-release.dmcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.ktcore/data/src/androidMain/kotlin/zed/rainxch/core/data/utils/AndroidClipboardHelper.ktcore/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ThemesRepositoryImpl.ktcore/data/src/jvmMain/kotlin/zed/rainxch/core/data/utils/DesktopClipboardHelper.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ThemesRepository.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/utils/ClipboardHelper.ktcore/presentation/src/commonMain/composeResources/values/strings.xmlfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.ktfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.ktfeature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.ktfeature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.ktfeature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.ktfeature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Appearance.ktfeature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.ktfeature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchEvent.ktfeature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.ktfeature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.ktfeature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.ktfeature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/GithubUrlParser.kt
core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ThemesRepositoryImpl.kt
Show resolved
Hide resolved
| 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"), |
There was a problem hiding this comment.
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.
| 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() |
There was a problem hiding this comment.
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
+ )
+ }
}
.../data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt
Show resolved
Hide resolved
| if (isEntirelyGithubUrls(action.query)) { | ||
| currentSearchJob?.cancel() | ||
| _state.update { | ||
| it.copy( | ||
| isLoading = false, | ||
| isLoadingMore = false, | ||
| errorMessage = null | ||
| ) | ||
| } | ||
| return |
There was a problem hiding this comment.
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.
.../presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/GithubUrlParser.kt
Show resolved
Hide resolved
…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.
There was a problem hiding this comment.
🧹 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
TranslationTargetis 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
whenexpressions to useTranslationTarget.AboutandTranslationTarget.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
📒 Files selected for processing (5)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.ktfeature/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`).
There was a problem hiding this comment.
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 | 🟠 MajorCancel 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 repopulaterepositoriesafter 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 | 🟠 MajorHTTP 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()andwhatsNew()calls are identical between theifandelsebranches—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
📒 Files selected for processing (6)
core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ThemesRepositoryImpl.ktfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.ktfeature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.ktfeature/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
| val responseText = httpClient.get( | ||
| "https://translate.googleapis.com/translate_a/single" |
There was a problem hiding this comment.
🧩 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.comis 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=gtxwhen 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
403on quota/rate issues, but it’s documented and configurable (quotas, billing, auth), unlike the unofficialgtxendpoint. [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:
- 1: https://maxkleiner1.medium.com/free-google-translator-api-ii-fcd7b82e4a99?utm_source=openai
- 2: https://www.npmjs.com/package/google-translate-api-x/v/10.4.2?utm_source=openai
- 3: https://cloud.google.com/translate/docs/reference/rest?utm_source=openai
- 4: https://policies.google.com/terms/archive/20051105-20070416?hl=en&utm_source=openai
- 5: https://cloud.google.com/translate/quotas?utm_source=openai
🌐 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 (
TranslateTextAPI) [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:
- 1: https://docs.cloud.google.com/translate/docs/basic/translate-text-basic?utm_source=openai
- 2: https://docs.cloud.google.com/translate/docs/advanced/translating-text-v3?utm_source=openai
- 3: https://docs.cloud.google.com/translate/docs/reference/rest?utm_source=openai
- 4: https://www.b4x.com/android/forum/threads/suggestion-comment-translator.168725/latest?utm_source=openai
- 5: https://developers.deepl.com/api-reference/translate?utm_source=openai
- 6: https://learn.microsoft.com/en-us/azure/ai-services/translator/text-translation-overview?utm_source=openai
- 7: https://docs.aws.amazon.com/translate/latest/APIReference/API_TranslateText.html?utm_source=openai
- 8: https://docs.libretranslate.com/?utm_source=openai
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.
| themesRepository.getAutoDetectClipboardLinks().collect { enabled -> | ||
| _state.update { it.copy(autoDetectClipboardEnabled = enabled) } | ||
| } |
There was a problem hiding this comment.
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.
…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.
TranslationRepositoryand its implementation using Ktor to interface with the Google Translate API, including text chunking for large documents and a simple LRU cache.LanguagePickerandTranslationControlsUI components to manage language selection and translation states.DetailsViewModelandDetailsStateto handle translation logic, including support for automatic device language detection.AboutandWhatsNewUI sections.SupportedLanguage,TranslationResult, andTranslationStatemodels.TranslationRepositoryin the KoindetailsModule.Summary by CodeRabbit
New Features
UI