From 2ea381183e06918c8532addfdcc71df23c3c8724 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Thu, 19 Feb 2026 22:12:10 -0500 Subject: [PATCH 1/7] Created API endpoint for ai translations --- pom.xml | 5 ++ .../TranslateProjectAPIController.java | 70 +++++++++++++++++-- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 314cf5200..93f8b1f22 100644 --- a/pom.xml +++ b/pom.xml @@ -557,6 +557,11 @@ lzstring4java 0.1 + + software.amazon.awssdk + translate + 2.40.15 + UTF-8 diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java index c21b3d509..197337970 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java @@ -2,14 +2,14 @@ import java.io.IOException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; import org.wise.portal.domain.project.impl.ProjectImpl; import org.wise.portal.domain.user.User; import org.wise.portal.service.project.ProjectService; @@ -18,7 +18,14 @@ import com.fasterxml.jackson.databind.node.ObjectNode; -@Controller +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.translate.TranslateClient; +import software.amazon.awssdk.services.translate.model.TranslateTextRequest; +import software.amazon.awssdk.services.translate.model.TranslateTextResponse; + +@RestController @RequestMapping("/api/author/project/translate") @Secured({ "ROLE_AUTHOR" }) public class TranslateProjectAPIController { @@ -32,8 +39,16 @@ public class TranslateProjectAPIController { @Autowired protected TranslateProjectService translateProjectService; + @Value("${aws.accessKeyId}") + private String accessKey; + + @Value("${aws.secretAccessKey}") + private String secretKey; + + @Value("${aws.region}") + private Region region; + @PostMapping("{projectId}/{locale}") - @ResponseBody protected void saveTranslations(Authentication auth, @PathVariable("projectId") ProjectImpl project, @PathVariable("locale") String locale, @RequestBody ObjectNode translations) throws IOException { @@ -42,4 +57,51 @@ protected void saveTranslations(Authentication auth, translateProjectService.saveTranslations(project, locale, translations.toString()); } } + + @PostMapping("translationSuggestions") + protected String getSuggestedTranslation(Authentication auth, @RequestBody ObjectNode objectNode) throws IOException, IllegalArgumentException { + String srcLang = objectNode.get("srcLang").asText(); + String targetLang = objectNode.get("targetLang").asText(); + String srcText = objectNode.get("srcText").asText(); + String srcLangCode = this.convertLanguageToAWSCode(srcLang); + String targetLangCode = this.convertLanguageToAWSCode(targetLang); + TranslateClient translateClient = buildTranslateClient(); + TranslateTextRequest request = buildTranslateTextRequest(srcText, srcLangCode, targetLangCode); + TranslateTextResponse textResponse = translateClient.translateText(request); + return textResponse.translatedText(); + } + + private TranslateClient buildTranslateClient() { + AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey); + return TranslateClient.builder() + .region(region) + .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .build(); + } + + private TranslateTextRequest buildTranslateTextRequest(String srcText, String srcLangCode, + String targetLangCode) { + return TranslateTextRequest.builder() + .text(srcText) + .sourceLanguageCode(srcLangCode) + .targetLanguageCode(targetLangCode) + .build(); + } + + private String convertLanguageToAWSCode(String language) throws IllegalArgumentException { + return switch (language) { + case "English" -> "en"; + case "Spanish" -> "es"; + case "Italian" -> "it"; + case "Japanese" -> "ja"; + case "German" -> "de"; + case "Chinese (Simplified)" -> "zh"; + case "Chinese (Traditional)" -> "zh-TW"; + case "Dutch" -> "nl"; + case "Korean" -> "ko"; + case "Vietnamese" -> "vi"; + default -> throw new IllegalArgumentException("Invalid language provided"); + }; + } + } From 54e2d652077ff750c725e9cb2cd2b52e01f6685f Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 20 Feb 2026 19:34:05 -0500 Subject: [PATCH 2/7] Translate with Mexican Spanish instead of basic Spanish --- .../author/project/TranslateProjectAPIController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java index 197337970..bcc2f7cec 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java @@ -91,7 +91,7 @@ private TranslateTextRequest buildTranslateTextRequest(String srcText, String sr private String convertLanguageToAWSCode(String language) throws IllegalArgumentException { return switch (language) { case "English" -> "en"; - case "Spanish" -> "es"; + case "Spanish" -> "es-MX"; case "Italian" -> "it"; case "Japanese" -> "ja"; case "German" -> "de"; From 28def20cb38fa00b8479656b1a0a3791883e95ab Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 13 Mar 2026 14:55:38 -0400 Subject: [PATCH 3/7] Updated sample app props --- src/main/resources/application-dockerdev-sample.properties | 7 +++++++ src/main/resources/application_sample.properties | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/main/resources/application-dockerdev-sample.properties b/src/main/resources/application-dockerdev-sample.properties index d3b2b0a73..c27db814b 100644 --- a/src/main/resources/application-dockerdev-sample.properties +++ b/src/main/resources/application-dockerdev-sample.properties @@ -42,6 +42,9 @@ spring.servlet.multipart.max-request-size=100MB # henry_verification_url - [url or leave empty] henry verification url # henry_scoring_url - [url or leave empty] henry scoring url # henry_client_id - [id or leave empty] henry client token id +# aws.accessKeyId - [key or leave empty] AWS Translate public key +# aws.secretAccessKey - [key or leave empty] AWS Translate secret key +# aws.region - [region or leave empty] AWS Translate server region wise.name=My Local WISE Development Instance wise.hostname=http://localhost:81 @@ -73,6 +76,10 @@ berkeley_cRater_scoring_url= berkeley_cRater_client_id= berkeley_cRater_password= +aws.accessKeyId= +aws.secretAccessKey= +aws.region= + ######### database properties ######### # Modify below as needed. diff --git a/src/main/resources/application_sample.properties b/src/main/resources/application_sample.properties index cba99ec23..f8baa0db9 100644 --- a/src/main/resources/application_sample.properties +++ b/src/main/resources/application_sample.properties @@ -42,6 +42,9 @@ spring.servlet.multipart.max-request-size=100MB # henry_verification_url - [url or leave empty] henry verification url # henry_scoring_url - [url or leave empty] henry scoring url # henry_client_id - [id or leave empty] henry client token id +# aws.accessKeyId - [key or leave empty] AWS Translate public key +# aws.secretAccessKey - [key or leave empty] AWS Translate secret key +# aws.region - [region or leave empty] AWS Translate server region wise.name=My Local WISE Development Instance wise.hostname=http://localhost:8080 @@ -73,6 +76,10 @@ berkeley_cRater_scoring_url= berkeley_cRater_client_id= berkeley_cRater_password= +aws.accessKeyId= +aws.secretAccessKey= +aws.region= + ######### database properties ######### # Modify below as needed. From a262a332151d27b31da35fb92d0c1b8e48055da0 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 13 Mar 2026 15:02:19 -0400 Subject: [PATCH 4/7] Handle missing app props, created TranslatableText class, and other suggested changes --- .../author/project/TranslatableText.java | 32 ++++++++++ .../TranslateProjectAPIController.java | 63 ++++++++----------- 2 files changed, 57 insertions(+), 38 deletions(-) create mode 100644 src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslatableText.java diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslatableText.java b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslatableText.java new file mode 100644 index 000000000..2fc8270ff --- /dev/null +++ b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslatableText.java @@ -0,0 +1,32 @@ +package org.wise.portal.presentation.web.controllers.author.project; + +import lombok.Getter; + +@Getter +public class TranslatableText { + private String srcLangCode; + private String targetLangCode; + private String srcText; + + public TranslatableText(String srcLang, String targetLang, String srcText) { + this.srcLangCode = this.convertLanguageToAWSCode(srcLang); + this.targetLangCode = this.convertLanguageToAWSCode(targetLang); + this.srcText = srcText; + } + + private String convertLanguageToAWSCode(String language) throws IllegalArgumentException { + return switch (language) { + case "English" -> "en"; + case "Spanish" -> "es-MX"; + case "Italian" -> "it"; + case "Japanese" -> "ja"; + case "German" -> "de"; + case "Chinese (Simplified)" -> "zh"; + case "Chinese (Traditional)" -> "zh-TW"; + case "Dutch" -> "nl"; + case "Korean" -> "ko"; + case "Vietnamese" -> "vi"; + default -> throw new IllegalArgumentException("Invalid language provided"); + }; + } +} diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java index bcc2f7cec..fe2bc205e 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java @@ -1,8 +1,10 @@ package org.wise.portal.presentation.web.controllers.author.project; import java.io.IOException; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PathVariable; @@ -10,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; import org.wise.portal.domain.project.impl.ProjectImpl; import org.wise.portal.domain.user.User; import org.wise.portal.service.project.ProjectService; @@ -39,14 +42,14 @@ public class TranslateProjectAPIController { @Autowired protected TranslateProjectService translateProjectService; - @Value("${aws.accessKeyId}") + @Value("${aws.accessKeyId:}") private String accessKey; - @Value("${aws.secretAccessKey}") + @Value("${aws.secretAccessKey:}") private String secretKey; - @Value("${aws.region}") - private Region region; + @Value("${aws.region:}") + private String region; @PostMapping("{projectId}/{locale}") protected void saveTranslations(Authentication auth, @@ -58,50 +61,34 @@ protected void saveTranslations(Authentication auth, } } - @PostMapping("translationSuggestions") - protected String getSuggestedTranslation(Authentication auth, @RequestBody ObjectNode objectNode) throws IOException, IllegalArgumentException { - String srcLang = objectNode.get("srcLang").asText(); - String targetLang = objectNode.get("targetLang").asText(); - String srcText = objectNode.get("srcText").asText(); - String srcLangCode = this.convertLanguageToAWSCode(srcLang); - String targetLangCode = this.convertLanguageToAWSCode(targetLang); - TranslateClient translateClient = buildTranslateClient(); - TranslateTextRequest request = buildTranslateTextRequest(srcText, srcLangCode, targetLangCode); - TranslateTextResponse textResponse = translateClient.translateText(request); - return textResponse.translatedText(); + @PostMapping("suggest") + protected String getSuggestedTranslation(Authentication auth, @RequestBody TranslatableText translatableText) throws IOException, IllegalArgumentException { + if (accessKey.equals("") || secretKey.equals("") || region.equals("")) { + throw new ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, + "Missing application properties necessary for AWS Translate" + ); + } else { + TranslateClient translateClient = buildTranslateClient(); + TranslateTextRequest request = buildTranslateTextRequest(translatableText); + TranslateTextResponse textResponse = translateClient.translateText(request); + return textResponse.translatedText(); + } } private TranslateClient buildTranslateClient() { AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey); return TranslateClient.builder() - .region(region) + .region(Region.of(region)) .credentialsProvider(StaticCredentialsProvider.create(credentials)) .build(); } - private TranslateTextRequest buildTranslateTextRequest(String srcText, String srcLangCode, - String targetLangCode) { + private TranslateTextRequest buildTranslateTextRequest(TranslatableText translatableText) { return TranslateTextRequest.builder() - .text(srcText) - .sourceLanguageCode(srcLangCode) - .targetLanguageCode(targetLangCode) + .text(translatableText.getSrcText()) + .sourceLanguageCode(translatableText.getSrcLangCode()) + .targetLanguageCode(translatableText.getTargetLangCode()) .build(); } - - private String convertLanguageToAWSCode(String language) throws IllegalArgumentException { - return switch (language) { - case "English" -> "en"; - case "Spanish" -> "es-MX"; - case "Italian" -> "it"; - case "Japanese" -> "ja"; - case "German" -> "de"; - case "Chinese (Simplified)" -> "zh"; - case "Chinese (Traditional)" -> "zh-TW"; - case "Dutch" -> "nl"; - case "Korean" -> "ko"; - case "Vietnamese" -> "vi"; - default -> throw new IllegalArgumentException("Invalid language provided"); - }; - } - } From 52591c70624fdedf8487d4f80cf928999072cf29 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 13 Mar 2026 20:02:34 -0400 Subject: [PATCH 5/7] Throw server error if translation fails --- .../TranslateProjectAPIController.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java index fe2bc205e..e2cbe8f25 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java @@ -62,7 +62,8 @@ protected void saveTranslations(Authentication auth, } @PostMapping("suggest") - protected String getSuggestedTranslation(Authentication auth, @RequestBody TranslatableText translatableText) throws IOException, IllegalArgumentException { + protected String getSuggestedTranslation(Authentication auth, @RequestBody TranslatableText translatableText) + throws IOException, IllegalArgumentException, ResponseStatusException { if (accessKey.equals("") || secretKey.equals("") || region.equals("")) { throw new ResponseStatusException( HttpStatus.INTERNAL_SERVER_ERROR, @@ -71,8 +72,7 @@ protected String getSuggestedTranslation(Authentication auth, @RequestBody Trans } else { TranslateClient translateClient = buildTranslateClient(); TranslateTextRequest request = buildTranslateTextRequest(translatableText); - TranslateTextResponse textResponse = translateClient.translateText(request); - return textResponse.translatedText(); + return this.translateText(translateClient, request); } } @@ -91,4 +91,17 @@ private TranslateTextRequest buildTranslateTextRequest(TranslatableText translat .targetLanguageCode(translatableText.getTargetLangCode()) .build(); } + + private String translateText(TranslateClient client, TranslateTextRequest request) throws ResponseStatusException { + TranslateTextResponse textResponse; + try { + textResponse = client.translateText(request); + } catch (Exception e) { + throw new ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, + "Translation failed" + ); + } + return textResponse.translatedText(); + } } From 8a982d81552ba8a48a079d6f1a0890e1c2bd534f Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Fri, 13 Mar 2026 20:03:52 -0400 Subject: [PATCH 6/7] Add translationServiceEnabled field to config depending on application properties --- .../controllers/author/project/AuthorAPIController.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/author/project/AuthorAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/AuthorAPIController.java index 6b3b243b0..4aa29178a 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/author/project/AuthorAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/AuthorAPIController.java @@ -400,6 +400,7 @@ protected HashMap getAuthorProjectConfig(Authentication auth, config.put("projectBaseURL", projectBaseURL); config.put("previewProjectURL", contextPath + "/preview/unit/" + project.getId()); config.put("chatGptEnabled", !StringUtils.isEmpty(appProperties.getProperty("OPENAI_API_KEY"))); + config.put("translationServiceEnabled", this.awsPropertiesConfigured()); config.put("cRaterRequestURL", contextPath + "/api/c-rater"); config.put("importStepsURL", contextPath + "/api/author/project/importSteps/" + project.getId()); @@ -424,6 +425,12 @@ protected HashMap getAuthorProjectConfig(Authentication auth, return config; } + private boolean awsPropertiesConfigured() { + return !(StringUtils.isEmpty(appProperties.getProperty("aws.accessKeyId")) + || StringUtils.isEmpty(appProperties.getProperty("aws.secretAccessKey")) + || StringUtils.isEmpty(appProperties.getProperty("aws.region"))); + } + /** * Get the run that uses the project id * From 7c93790fa84455e9895f91a0acfbbf8f004c22d5 Mon Sep 17 00:00:00 2001 From: Aaron Detre Date: Mon, 16 Mar 2026 17:44:28 -0400 Subject: [PATCH 7/7] Fixed failing test --- .../controllers/author/project/AuthorAPIControllerTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/author/project/AuthorAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/author/project/AuthorAPIControllerTest.java index 042fddeaa..fa766eddf 100644 --- a/src/test/java/org/wise/portal/presentation/web/controllers/author/project/AuthorAPIControllerTest.java +++ b/src/test/java/org/wise/portal/presentation/web/controllers/author/project/AuthorAPIControllerTest.java @@ -81,6 +81,9 @@ public void getAuthorProjectConfig_HasProjectRun_ReturnCanGradeStudentWork() thr expect(appProperties.getProperty("curriculum_base_www")) .andReturn("http://localhost:8080/curriculum"); expect(appProperties.getProperty("OPENAI_API_KEY")).andReturn("OPENAPIKEY"); + expect(appProperties.getProperty("aws.accessKeyId")).andReturn("ACCESSKEY"); + expect(appProperties.getProperty("aws.secretAccessKey")).andReturn("SECRETKEY"); + expect(appProperties.getProperty("aws.region")).andReturn("us-west-1"); replay(appProperties); Map config = authorAPIController.getAuthorProjectConfig(teacherAuth, request, project1);