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..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 c21b3d509..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 @@ -1,15 +1,18 @@ 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.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.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; @@ -18,7 +21,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 +42,16 @@ public class TranslateProjectAPIController { @Autowired protected TranslateProjectService translateProjectService; + @Value("${aws.accessKeyId:}") + private String accessKey; + + @Value("${aws.secretAccessKey:}") + private String secretKey; + + @Value("${aws.region:}") + private String 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 +60,48 @@ protected void saveTranslations(Authentication auth, translateProjectService.saveTranslations(project, locale, translations.toString()); } } + + @PostMapping("suggest") + 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..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. 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);