Batch translate all files & Custom Open-AI API support - #85
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds batch translation functionality for all project files and support for custom OpenAI-compatible API endpoints (enabling use of local models like LMStudio and qwen3-30b).
Key Changes:
- Adds a "Batch AI translation (All Files)" button that sequentially processes all translation files in a project
- Introduces custom OpenAI base URL configuration and custom model name support for OpenAI-compatible providers
- Enables editable model selection in UI to support arbitrary model names
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| autopo-i18n/po/autopo.pot | Adds translation strings for the new "Batch AI translation (All Files)" feature |
| TranslationsTableToolbar.java | Implements TranslateAllFilesButton class with sequential file processing logic |
| OpenAiModelDescriptor.java | Adds HTTP client configuration, base URL support, and custom model name handling |
| OpenAISettings.java | Updates UI to support editable model names and custom base URL field |
| OpenAIPersistentProperty.java | Adds BASE_URL persistent property |
| module-info.java | Adds required module dependencies for HTTP client functionality |
| pom.xml | Adds langchain4j-http-client-jdk dependency |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (n) { | ||
| ofNullable(subscription[0]).ifPresent(Subscription::unsubscribe); | ||
| // Move to next file after translation completes | ||
| processNextFile(iterator, aiModelDescriptor, description); | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
The subscription array pattern creates a potential memory leak. If the translation never completes (e.g., due to an error or cancellation), the subscription will never be unsubscribed and the listener will remain active indefinitely. Consider handling error cases or adding a cleanup mechanism to ensure subscriptions are always released.
| if (n) { | |
| ofNullable(subscription[0]).ifPresent(Subscription::unsubscribe); | |
| // Move to next file after translation completes | |
| processNextFile(iterator, aiModelDescriptor, description); | |
| } | |
| }); | |
| } | |
| // Always unsubscribe to avoid leaking the listener, regardless of outcome | |
| ofNullable(subscription[0]).ifPresent(Subscription::unsubscribe); | |
| // Move to next file after translation completes or in case of failure/cancellation | |
| processNextFile(iterator, aiModelDescriptor, description); | |
| }); | |
| } | |
| } |
| JdkHttpClientBuilder customHttpClientBuilder = JdkHttpClient.builder() | ||
| .httpClientBuilder(HttpClient.newBuilder() | ||
| .version(HttpClient.Version.HTTP_1_1) // Specify HTTP/1.1 for certain local models like vLLM | ||
| .connectTimeout(Duration.ofSeconds(10))) // Set connection timeout | ||
| .readTimeout(Duration.ofSeconds(60)); // Set read timeout |
There was a problem hiding this comment.
The code duplicates HTTP client configuration logic between translationModel() and validationModel() methods. Consider extracting this configuration into a private helper method to improve maintainability and ensure consistency.
| var baseUrl = repo.getString(OpenAIPersistentProperty.BASE_URL.key(), (String) null); | ||
| if (isNotBlank(baseUrl)) { | ||
| builder.baseUrl(baseUrl); | ||
| } else { | ||
| builder.baseUrl(DEFAULT_OPENAI_BASE_URL); | ||
| } |
There was a problem hiding this comment.
The base URL configuration logic is duplicated between translationModel() and validationModel() methods. Consider extracting this logic into a private helper method to improve maintainability and reduce duplication.
| var modelName = repo.getString(OpenAIPersistentProperty.MODEL_NAME.key(), GPT_4.name()); | ||
| try { | ||
| builder.modelName(OpenAiChatModelName.valueOf(modelName)); | ||
| } catch (IllegalArgumentException e) { | ||
| // Custom model name not in enum, use as string | ||
| builder.modelName(modelName); | ||
| } |
There was a problem hiding this comment.
The model name configuration logic with exception handling is duplicated between translationModel() and validationModel() methods. Consider extracting this logic into a private helper method to improve maintainability and ensure consistent error handling.
| add(new Label(i18n().tr("Base URL:")), 0, 4); | ||
| var urlField = new TextField(); | ||
| urlField.setPromptText("https://api.openai.com/v1/"); | ||
| ofNullable(repo.getString(OpenAIPersistentProperty.BASE_URL.key(), (String) null)).ifPresent(urlField::setText); | ||
| urlField.textProperty().subscribe((o, n) -> repo.saveString(OpenAIPersistentProperty.BASE_URL.key(), n)); | ||
| setFillWidth(urlField, true); | ||
| add(urlField, 1, 4, 2, 1); |
There was a problem hiding this comment.
The Base URL field lacks input validation. Users could enter malformed URLs or non-HTTPS endpoints which could lead to runtime errors or security issues. Consider adding URL format validation and potentially restricting to HTTPS for security purposes.
| try { | ||
| builder.modelName(OpenAiChatModelName.valueOf(modelName)); | ||
| } catch (IllegalArgumentException e) { | ||
| // Custom model name not in enum, use as string | ||
| builder.modelName(modelName); | ||
| } |
There was a problem hiding this comment.
The IllegalArgumentException catch block silently falls back to using the model name as a string without logging or notifying the user. This could make it difficult to debug issues when a user enters an invalid model name. Consider logging the exception or providing user feedback when falling back to custom model names.
| add(new Label(i18n().tr("Base URL:")), 0, 4); | ||
| var urlField = new TextField(); | ||
| urlField.setPromptText("https://api.openai.com/v1/"); | ||
| ofNullable(repo.getString(OpenAIPersistentProperty.BASE_URL.key(), (String) null)).ifPresent(urlField::setText); | ||
| urlField.textProperty().subscribe((o, n) -> repo.saveString(OpenAIPersistentProperty.BASE_URL.key(), n)); | ||
| setFillWidth(urlField, true); | ||
| add(urlField, 1, 4, 2, 1); | ||
|
|
||
| Button clearButton = new Button(i18n().tr("Clear")); | ||
| clearButton.setTooltip(new Tooltip(i18n().tr("Clear OpenAI settings"))); | ||
| clearButton.setGraphic(FontIcon.of(FluentUiFilledAL.ERASER_24)); | ||
| clearButton.setOnAction(e -> { | ||
| repo.clean(); | ||
| apiField.setText(""); | ||
| modelCombo.getSelectionModel().clearSelection(); | ||
| urlField.setText(""); | ||
| }); | ||
| add(clearButton, 0, 3); |
There was a problem hiding this comment.
The Base URL field is added at row 4, but the Clear button is at row 3, causing the UI layout to be inconsistent with a gap between the Temperature field (row 2) and the Clear button (row 3). The Base URL should be at row 3, and the Clear button should be moved to row 4 to maintain a logical top-to-bottom flow without gaps.
| final Subscription[] subscription = new Subscription[1]; | ||
| subscription[0] = poFile.status().subscribe(status -> { | ||
| if (status == LoadingStatus.LOADED) { | ||
| ofNullable(subscription[0]).ifPresent(Subscription::unsubscribe); | ||
| translateCurrentFile(aiModelDescriptor, description, iterator); | ||
| } else if (status == LoadingStatus.ERROR) { | ||
| ofNullable(subscription[0]).ifPresent(Subscription::unsubscribe); | ||
| // Skip this file and continue with next | ||
| processNextFile(iterator, aiModelDescriptor, description); | ||
| } | ||
| }); |
There was a problem hiding this comment.
The subscription array pattern used here creates a potential memory leak. The subscription is stored in an array to allow self-reference, but if the status never becomes LOADED or ERROR, the subscription will never be unsubscribed and the listener will remain active indefinitely. Consider adding a timeout or cleanup mechanism to ensure the subscription is always eventually unsubscribed.
Hello my mates!
I proudly vibe-coded something 🤠 I can't verify myself from a Java perspective.
BUT
it works great and has really helped me use the local LMStudio to make translations for my micro-project.
That you can, btw, check out here bbq-me.org it's a pretty fun tool that helps me find places to hang out with friends on the weekends 🌭
I want to leave this in the pull request here to not abandon the work that I believe can help many other people who have found this program very useful, but I immediately got tired of clicking on each file manually and waiting to move on to the next one. And again after adding each new line for translation...
So