diff --git a/pom.xml b/pom.xml
index 5ab62c717..0b07cc732 100644
--- a/pom.xml
+++ b/pom.xml
@@ -553,6 +553,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/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
*
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..b77090b3a
--- /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 {
+ protected String srcLangCode;
+ protected String targetLangCode;
+ protected 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 c21b3d509..a7952b8e5 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,15 +1,15 @@
package org.wise.portal.presentation.web.controllers.author.project;
import java.io.IOException;
+
import org.springframework.beans.factory.annotation.Autowired;
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,8 +18,8 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
-@Controller
-@RequestMapping("/api/author/project/translate")
+@RestController
+@RequestMapping("/api/author/project/translate/{projectId}/{locale}")
@Secured({ "ROLE_AUTHOR" })
public class TranslateProjectAPIController {
@@ -32,8 +32,7 @@ public class TranslateProjectAPIController {
@Autowired
protected TranslateProjectService translateProjectService;
- @PostMapping("{projectId}/{locale}")
- @ResponseBody
+ @PostMapping
protected void saveTranslations(Authentication auth,
@PathVariable("projectId") ProjectImpl project, @PathVariable("locale") String locale,
@RequestBody ObjectNode translations) throws IOException {
diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslationSuggestionAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslationSuggestionAPIController.java
new file mode 100644
index 000000000..08d76ac9e
--- /dev/null
+++ b/src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslationSuggestionAPIController.java
@@ -0,0 +1,79 @@
+package org.wise.portal.presentation.web.controllers.author.project;
+
+import java.io.IOException;
+
+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.PostMapping;
+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 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/suggest")
+@Secured({ "ROLE_AUTHOR" })
+public class TranslationSuggestionAPIController {
+
+ @Value("${aws.accessKeyId:}")
+ private String accessKey;
+
+ @Value("${aws.secretAccessKey:}")
+ private String secretKey;
+
+ @Value("${aws.region:}")
+ private String region;
+
+ @PostMapping
+ 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,
+ "Missing application properties necessary for AWS Translate"
+ );
+ } else {
+ TranslateClient translateClient = buildTranslateClient();
+ TranslateTextRequest request = buildTranslateTextRequest(translatableText);
+ return this.translateText(translateClient, request);
+ }
+ }
+
+ private TranslateClient buildTranslateClient() {
+ AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);
+ return TranslateClient.builder()
+ .region(Region.of(region))
+ .credentialsProvider(StaticCredentialsProvider.create(credentials))
+ .build();
+ }
+
+ private TranslateTextRequest buildTranslateTextRequest(TranslatableText translatableText) {
+ return TranslateTextRequest.builder()
+ .text(translatableText.getSrcText())
+ .sourceLanguageCode(translatableText.getSrcLangCode())
+ .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();
+ }
+}
diff --git a/src/main/resources/application-dockerdev-sample.properties b/src/main/resources/application-dockerdev-sample.properties
index d3b2b0a73..dd8dde2dd 100644
--- a/src/main/resources/application-dockerdev-sample.properties
+++ b/src/main/resources/application-dockerdev-sample.properties
@@ -216,6 +216,13 @@ system-wide-salt=secret
#speech-to-text.aws.region=
#speech-to-text.aws.identity-pool-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
+aws.accessKeyId=
+aws.secretAccessKey=
+aws.region=
+
# OpenAI and AWS Bedrock Chat endpoints (optional)
#OPENAI_API_KEY=
#aws.bedrock.api.key=
diff --git a/src/main/resources/application_sample.properties b/src/main/resources/application_sample.properties
index cba99ec23..29bac3956 100644
--- a/src/main/resources/application_sample.properties
+++ b/src/main/resources/application_sample.properties
@@ -216,6 +216,13 @@ system-wide-salt=secret
#speech-to-text.aws.region=
#speech-to-text.aws.identity-pool-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
+aws.accessKeyId=
+aws.secretAccessKey=
+aws.region=
+
# OpenAI and AWS Bedrock Chat endpoints (optional)
#OPENAI_API_KEY=
#aws.bedrock.api.key=
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);
diff --git a/src/test/java/org/wise/portal/presentation/web/controllers/author/project/TranslationSuggestionAPIControllerTest.java b/src/test/java/org/wise/portal/presentation/web/controllers/author/project/TranslationSuggestionAPIControllerTest.java
new file mode 100644
index 000000000..8a99d888a
--- /dev/null
+++ b/src/test/java/org/wise/portal/presentation/web/controllers/author/project/TranslationSuggestionAPIControllerTest.java
@@ -0,0 +1,31 @@
+package org.wise.portal.presentation.web.controllers.author.project;
+
+import org.easymock.EasyMockExtension;
+import org.easymock.TestSubject;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.server.ResponseStatusException;
+import org.wise.portal.presentation.web.controllers.APIControllerTest;
+
+@ExtendWith(EasyMockExtension.class)
+public class TranslationSuggestionAPIControllerTest extends APIControllerTest {
+
+ @TestSubject
+ private final TranslationSuggestionAPIController controller = new TranslationSuggestionAPIController();
+
+ @Test
+ public void getSuggestedTranslation_ThrowIfPropertiesEmpty() throws Exception {
+ ReflectionTestUtils.setField(controller, "accessKey", "");
+ ReflectionTestUtils.setField(controller, "secretKey", "");
+ ReflectionTestUtils.setField(controller, "region", "");
+
+ TranslatableText tt = new TranslatableText("English", "Spanish", "text to translate");
+
+ assertThrows(ResponseStatusException.class, () -> {
+ controller.getSuggestedTranslation(teacherAuth, tt);
+ });
+ }
+}
+