feat: add LibreTranslate + DeepL translation providers#649
Conversation
|
Caution Review failedPull request was closed or merged during review WalkthroughAdds LibreTranslate, DeepL, and Microsoft translators; extends domain enum and TweaksRepository contract; persists Libre/DeepL credentials in KSafe; wires translators in TranslationRepositoryImpl with a default Libre URL; and adds ViewModel, UI forms, events, and strings to manage credentials and provider selection. ChangesTranslation Provider Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
core/presentation/src/commonMain/composeResources/values/strings.xml (1)
904-905:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate the provider description to reflect all available providers.
Line 904 still describes only Google/Youdao, which is now outdated after adding LibreTranslate and DeepL.
Suggested text update
- <string name="translation_provider_description">Google works globally without configuration. Youdao works from mainland China but requires API credentials from Youdao's developer portal.</string> + <string name="translation_provider_description">Google works globally without configuration. Youdao requires API credentials from Youdao's developer portal. LibreTranslate supports self-hosted instances for stronger privacy control. DeepL requires an auth key (Free and Pro tiers use different endpoints automatically).</string>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/presentation/src/commonMain/composeResources/values/strings.xml` around lines 904 - 905, The string resource translation_provider_description is outdated (mentions only Google and Youdao); update its text to include all current providers (Google, Youdao, LibreTranslate, DeepL) and brief notes about availability/requirements (e.g., Google — global, Youdao — mainland China with API credentials, LibreTranslate — self-hostable/public instances, DeepL — requires API key and may have regional limitations) so the new description accurately reflects all providers; locate and edit the value of translation_provider_description (and adjust translation_provider_google if needed) in the strings.xml entry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/DeeplTranslator.kt`:
- Line 65: The DeeplTranslator translation extraction currently uses
first["text"]?.jsonPrimitive?.content.orEmpty() which silently returns an empty
string when the required "text" field is missing; change this to treat a missing
or non-primitive "text" as an API error by validating the presence and type of
first["text"] and throwing the same kind of exception used elsewhere in this
class (e.g., the existing API error handling at the checks around lines 59 and
64) instead of returning an empty string—update the code that reads translation
(the variable named translation and the access first["text"]) to throw a
descriptive exception when the field is absent or invalid so failures fail fast.
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/LibreTranslator.kt`:
- Line 53: The assignment to translation in LibreTranslator currently uses
root["translatedText"]?.jsonPrimitive?.content.orEmpty() which silently returns
an empty string when the field is missing; change this to fail-fast by checking
root["translatedText"] and throwing an exception (similar to the existing error
handling for "error" responses) if the field is absent or not a primitive
string. Locate the LibreTranslator function where translation is computed
(reference symbol: translation and root["translatedText"]) and replace the
orEmpty() usage with a null-check that throws a descriptive exception (e.g.,
IllegalStateException or a custom error) so missing translatedText is surfaced
immediately.
In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt`:
- Line 467: The placeholder string in Translation.kt is hardcoded as
Text("https://translate.example.com"); replace this with a localized resource by
adding a string resource named translation_libre_base_url_placeholder (value
"https://translate.example.com") and use stringResource(...) inside the
placeholder lambda (i.e., change Text("...") to
Text(stringResource(R.string.translation_libre_base_url_placeholder))) and add
the necessary import for stringResource; this ensures the LibreTranslate URL
placeholder is translatable.
---
Outside diff comments:
In `@core/presentation/src/commonMain/composeResources/values/strings.xml`:
- Around line 904-905: The string resource translation_provider_description is
outdated (mentions only Google and Youdao); update its text to include all
current providers (Google, Youdao, LibreTranslate, DeepL) and brief notes about
availability/requirements (e.g., Google — global, Youdao — mainland China with
API credentials, LibreTranslate — self-hostable/public instances, DeepL —
requires API key and may have regional limitations) so the new description
accurately reflects all providers; locate and edit the value of
translation_provider_description (and adjust translation_provider_google if
needed) in the strings.xml entry.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4d724702-e12a-4e14-b4b6-6e561b6abb68
📒 Files selected for processing (13)
core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/TranslationProvider.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.ktcore/presentation/src/commonMain/composeResources/values/strings.xmlfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.ktfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/DeeplTranslator.ktfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/LibreTranslator.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt
| value = state.libreTranslateBaseUrl, | ||
| onValueChange = { onAction(TweaksAction.OnLibreTranslateBaseUrlChanged(it)) }, | ||
| label = { Text(stringResource(Res.string.translation_libre_base_url)) }, | ||
| placeholder = { Text("https://translate.example.com") }, |
There was a problem hiding this comment.
Localize the LibreTranslate URL placeholder.
This placeholder is hardcoded and won’t be translatable.
Suggested fix
- placeholder = { Text("https://translate.example.com") },
+ placeholder = { Text(stringResource(Res.string.translation_libre_base_url_placeholder)) },Add a string resource:
<string name="translation_libre_base_url_placeholder">https://translate.example.com</string>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt`
at line 467, The placeholder string in Translation.kt is hardcoded as
Text("https://translate.example.com"); replace this with a localized resource by
adding a string resource named translation_libre_base_url_placeholder (value
"https://translate.example.com") and use stringResource(...) inside the
placeholder lambda (i.e., change Text("...") to
Text(stringResource(R.string.translation_libre_base_url_placeholder))) and add
the necessary import for stringResource; this ensures the LibreTranslate URL
placeholder is translatable.
Greptile SummaryAdds LibreTranslate, DeepL, and Microsoft Translator as new translation providers, following the existing Google/Youdao architecture (enum entry →
Confidence Score: 5/5Safe to merge; all three translators are well-guarded and the credential storage follows the existing KSafe pattern The previous round's response-parsing and language-mapping bugs have all been addressed. The two remaining findings are non-blocking: one is a dead condition in the LibreTranslate shouldActivate expression that never causes wrong behavior, and the other is a language-mapping default that may surprise non-American English users but does not break functionality. DeeplTranslator.kt (en → EN-US mapping) and TweaksViewModel.kt (dead branch in shouldActivate) Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[resolveTranslator] --> B{TranslationProvider}
B --> C[GOOGLE → GoogleTranslator]
B --> D[YOUDAO → YoudaoTranslator]
B --> E[LIBRE_TRANSLATE]
B --> F[DEEPL → DeeplTranslator]
B --> G[MICROSOFT → MicrosoftTranslator]
E --> E1{baseUrl blank?}
E1 -- yes --> E2[use LIBRE_TRANSLATE_DEFAULT_URL]
E1 -- no --> E3[use user-configured URL]
E2 --> E4[LibreTranslator]
E3 --> E4
F --> F1{authKey ends with :fx?}
F1 -- yes --> F2[api-free.deepl.com]
F1 -- no --> F3[api.deepl.com]
F2 --> F4[translate]
F3 --> F4
G --> G1{subscriptionRegion blank or global?}
G1 -- yes --> G2[No region header]
G1 -- no --> G3[Add Ocp-Apim-Subscription-Region header]
G2 --> G4[api.cognitive.microsofttranslator.com]
G3 --> G4
Reviews (4): Last reviewed commit: "cr: guard HTTP status + surface error me..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`:
- Around line 969-978: The code currently commits
TranslationProvider.LIBRE_TRANSLATE immediately; change it to require either a
non-empty user URL (draftTranslationProvider) or an explicit user confirmation
before calling tweaksRepository.setTranslationProvider. Instead of directly
clearing draft and launching the repository update in the
TranslationProvider.LIBRE_TRANSLATE branch, update the draft state to preserve
any user-entered URL via _state.update { it.copy(draftTranslationProvider = /*
keep or set value */) }, send an event (e.g.,
TweaksEvent.RequestLibreTranslateConfirmation) with _events.send to trigger a
confirmation dialog, and only perform
tweaksRepository.setTranslationProvider(...) and send
TweaksEvent.OnTranslationProviderSaved from the confirmation handler (not here)
once the user either confirms “use public mirror” or provides a non-empty URL.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a63ba7b3-dda8-44b2-9dea-60810d706d0f
📒 Files selected for processing (4)
core/presentation/src/commonMain/composeResources/values/strings.xmlfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt
✅ Files skipped from review due to trivial changes (1)
- core/presentation/src/commonMain/composeResources/values/strings.xml
| TranslationProvider.LIBRE_TRANSLATE -> { | ||
| // No gating — repository falls back to the | ||
| // public Disroot mirror when no URL configured, | ||
| // so first-tap "just works" without going | ||
| // through a config dialog. | ||
| _state.update { it.copy(draftTranslationProvider = null) } | ||
| viewModelScope.launch { | ||
| tweaksRepository.setTranslationProvider(action.provider) | ||
| _events.send(TweaksEvent.OnTranslationProviderSaved) | ||
| } |
There was a problem hiding this comment.
Require explicit consent/config before activating LibreTranslate.
This branch commits LIBRE_TRANSLATE immediately, which can route user text to the public mirror without explicit confirmation of endpoint choice. Please gate activation behind either a non-empty user-provided URL or an explicit “use public mirror” confirmation step.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`
around lines 969 - 978, The code currently commits
TranslationProvider.LIBRE_TRANSLATE immediately; change it to require either a
non-empty user URL (draftTranslationProvider) or an explicit user confirmation
before calling tweaksRepository.setTranslationProvider. Instead of directly
clearing draft and launching the repository update in the
TranslationProvider.LIBRE_TRANSLATE branch, update the draft state to preserve
any user-entered URL via _state.update { it.copy(draftTranslationProvider = /*
keep or set value */) }, send an event (e.g.,
TweaksEvent.RequestLibreTranslateConfirmation) with _events.send to trigger a
confirmation dialog, and only perform
tweaksRepository.setTranslationProvider(...) and send
TweaksEvent.OnTranslationProviderSaved from the confirmation handler (not here)
once the user either confirms “use public mirror” or provides a non-empty URL.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt (1)
220-223:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRemove implicit LibreTranslate public-mirror fallback.
Line 222 falls back to a public endpoint (Line 323) when URL is unset, which can send user text to a third party without explicit opt-in. For this flow, missing URL should be treated as not configured.
Suggested fix
+import zed.rainxch.details.data.translation.TranslationProviderNotConfiguredException @@ TranslationProvider.LIBRE_TRANSLATE -> { val configured = tweaksRepository.getLibreTranslateBaseUrl().first() - val baseUrl = configured.takeIf { it.isNotBlank() } ?: LIBRE_TRANSLATE_DEFAULT_URL + val baseUrl = configured.trim().takeIf { it.isNotEmpty() } + ?: throw TranslationProviderNotConfiguredException( + "LibreTranslate instance URL not configured", + ) val apiKey = tweaksRepository.getLibreTranslateApiKey().first().takeIf { it.isNotBlank() } LibreTranslator( httpClient = { httpClient }, json = json, baseUrl = baseUrl, apiKey = apiKey, ) } @@ - private const val LIBRE_TRANSLATE_DEFAULT_URL = "https://translate.disroot.org"Also applies to: 319-324
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt` around lines 220 - 223, The code currently falls back to LIBRE_TRANSLATE_DEFAULT_URL when tweaksRepository.getLibreTranslateBaseUrl() is blank (in TranslationRepositoryImpl where TranslationProvider.LIBRE_TRANSLATE is handled), which sends text to a public mirror; remove that implicit fallback by making baseUrl = configured.takeIf { it.isNotBlank() } (i.e. null when unset) and update the downstream logic (the LibreTranslate request path in TranslationRepositoryImpl) to treat a null/blank baseUrl as "not configured" (return an error/skip provider) rather than using LIBRE_TRANSLATE_DEFAULT_URL; apply the same change to the other LibreTranslate handling block that uses LIBRE_TRANSLATE_DEFAULT_URL.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/MicrosoftTranslator.kt`:
- Around line 72-79: The code in MicrosoftTranslator that builds the
`translation` value currently uses `.orEmpty()` which masks missing or malformed
responses; change it to fail-fast by replacing the `.orEmpty()` with a
null-check that throws a clear exception (e.g., IllegalStateException or a
domain-specific exception) when the chain from
`first["translations"]?.jsonArray?.firstOrNull()?.jsonObject?.get("text")?.jsonPrimitive?.content`
is null or blank; update the assignment to `translation` in MicrosoftTranslator
so missing translation text throws with a descriptive message instead of
returning an empty string.
---
Outside diff comments:
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt`:
- Around line 220-223: The code currently falls back to
LIBRE_TRANSLATE_DEFAULT_URL when tweaksRepository.getLibreTranslateBaseUrl() is
blank (in TranslationRepositoryImpl where TranslationProvider.LIBRE_TRANSLATE is
handled), which sends text to a public mirror; remove that implicit fallback by
making baseUrl = configured.takeIf { it.isNotBlank() } (i.e. null when unset)
and update the downstream logic (the LibreTranslate request path in
TranslationRepositoryImpl) to treat a null/blank baseUrl as "not configured"
(return an error/skip provider) rather than using LIBRE_TRANSLATE_DEFAULT_URL;
apply the same change to the other LibreTranslate handling block that uses
LIBRE_TRANSLATE_DEFAULT_URL.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ac0ba816-9f60-446c-a203-5180b912bcf5
📒 Files selected for processing (12)
core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/TranslationProvider.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.ktcore/presentation/src/commonMain/composeResources/values/strings.xmlfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.ktfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/MicrosoftTranslator.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.ktfeature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt
✅ Files skipped from review due to trivial changes (1)
- core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/TranslationProvider.kt
Summary
Adds two privacy-respecting translation providers per #644:
:fxsuffix) routes toapi-free.deepl.com; Pro routes toapi.deepl.com. Free tier may use submitted text to train models — disclaimer surfaced in UI.Architecture
Follows the existing Google/Youdao pattern:
TranslationProviderenum gainsLIBRE_TRANSLATE+DEEPLLibreTranslator+DeeplTranslatorimplementTranslatorTweaksRepositorygains KSafe-encrypted prefs (libre_translate_base_url,libre_translate_api_key,deepl_auth_key)TranslationRepositoryImpl.resolveTranslator()routes to new providersTest plan
:fxkey → Save → translate → verify free endpoint hit:fxkey → verify pro endpoint hitTranslationProviderNotConfiguredExceptionsurfaced in UICloses #644
Summary by CodeRabbit
New Features
UI Changes