Skip to content

Commit e86ed66

Browse files
authored
feat(Authoring): Translate with AI (#314)
1 parent 209e230 commit e86ed66

9 files changed

Lines changed: 176 additions & 6 deletions

File tree

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,11 @@
553553
<artifactId>lzstring4java</artifactId>
554554
<version>0.1</version>
555555
</dependency>
556+
<dependency>
557+
<groupId>software.amazon.awssdk</groupId>
558+
<artifactId>translate</artifactId>
559+
<version>2.40.15</version>
560+
</dependency>
556561
</dependencies>
557562
<properties>
558563
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

src/main/java/org/wise/portal/presentation/web/controllers/author/project/AuthorAPIController.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ protected HashMap<String, Object> getAuthorProjectConfig(Authentication auth,
400400
config.put("projectBaseURL", projectBaseURL);
401401
config.put("previewProjectURL", contextPath + "/preview/unit/" + project.getId());
402402
config.put("chatGptEnabled", !StringUtils.isEmpty(appProperties.getProperty("openai.api.key")));
403+
config.put("translationServiceEnabled", this.awsPropertiesConfigured());
403404
config.put("cRaterRequestURL", contextPath + "/api/c-rater");
404405
config.put("importStepsURL",
405406
contextPath + "/api/author/project/importSteps/" + project.getId());
@@ -424,6 +425,12 @@ protected HashMap<String, Object> getAuthorProjectConfig(Authentication auth,
424425
return config;
425426
}
426427

428+
private boolean awsPropertiesConfigured() {
429+
return !(StringUtils.isEmpty(appProperties.getProperty("aws.accessKeyId"))
430+
|| StringUtils.isEmpty(appProperties.getProperty("aws.secretAccessKey"))
431+
|| StringUtils.isEmpty(appProperties.getProperty("aws.region")));
432+
}
433+
427434
/**
428435
* Get the run that uses the project id
429436
*
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.wise.portal.presentation.web.controllers.author.project;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public class TranslatableText {
7+
protected String srcLangCode;
8+
protected String targetLangCode;
9+
protected String srcText;
10+
11+
public TranslatableText(String srcLang, String targetLang, String srcText) {
12+
this.srcLangCode = this.convertLanguageToAWSCode(srcLang);
13+
this.targetLangCode = this.convertLanguageToAWSCode(targetLang);
14+
this.srcText = srcText;
15+
}
16+
17+
private String convertLanguageToAWSCode(String language) throws IllegalArgumentException {
18+
return switch (language) {
19+
case "English" -> "en";
20+
case "Spanish" -> "es-MX";
21+
case "Italian" -> "it";
22+
case "Japanese" -> "ja";
23+
case "German" -> "de";
24+
case "Chinese (Simplified)" -> "zh";
25+
case "Chinese (Traditional)" -> "zh-TW";
26+
case "Dutch" -> "nl";
27+
case "Korean" -> "ko";
28+
case "Vietnamese" -> "vi";
29+
default -> throw new IllegalArgumentException("Invalid language provided");
30+
};
31+
}
32+
}

src/main/java/org/wise/portal/presentation/web/controllers/author/project/TranslateProjectAPIController.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
package org.wise.portal.presentation.web.controllers.author.project;
22

33
import java.io.IOException;
4+
45
import org.springframework.beans.factory.annotation.Autowired;
56
import org.springframework.security.access.annotation.Secured;
67
import org.springframework.security.core.Authentication;
7-
import org.springframework.stereotype.Controller;
88
import org.springframework.web.bind.annotation.PathVariable;
99
import org.springframework.web.bind.annotation.PostMapping;
1010
import org.springframework.web.bind.annotation.RequestBody;
1111
import org.springframework.web.bind.annotation.RequestMapping;
12-
import org.springframework.web.bind.annotation.ResponseBody;
12+
import org.springframework.web.bind.annotation.RestController;
1313
import org.wise.portal.domain.project.impl.ProjectImpl;
1414
import org.wise.portal.domain.user.User;
1515
import org.wise.portal.service.project.ProjectService;
@@ -18,8 +18,8 @@
1818

1919
import com.fasterxml.jackson.databind.node.ObjectNode;
2020

21-
@Controller
22-
@RequestMapping("/api/author/project/translate")
21+
@RestController
22+
@RequestMapping("/api/author/project/translate/{projectId}/{locale}")
2323
@Secured({ "ROLE_AUTHOR" })
2424
public class TranslateProjectAPIController {
2525

@@ -32,8 +32,7 @@ public class TranslateProjectAPIController {
3232
@Autowired
3333
protected TranslateProjectService translateProjectService;
3434

35-
@PostMapping("{projectId}/{locale}")
36-
@ResponseBody
35+
@PostMapping
3736
protected void saveTranslations(Authentication auth,
3837
@PathVariable("projectId") ProjectImpl project, @PathVariable("locale") String locale,
3938
@RequestBody ObjectNode translations) throws IOException {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.wise.portal.presentation.web.controllers.author.project;
2+
3+
import java.io.IOException;
4+
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.security.access.annotation.Secured;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.web.bind.annotation.PostMapping;
10+
import org.springframework.web.bind.annotation.RequestBody;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RestController;
13+
import org.springframework.web.server.ResponseStatusException;
14+
15+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
16+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
17+
import software.amazon.awssdk.regions.Region;
18+
import software.amazon.awssdk.services.translate.TranslateClient;
19+
import software.amazon.awssdk.services.translate.model.TranslateTextRequest;
20+
import software.amazon.awssdk.services.translate.model.TranslateTextResponse;
21+
22+
@RestController
23+
@RequestMapping("/api/author/project/translate/suggest")
24+
@Secured({ "ROLE_AUTHOR" })
25+
public class TranslationSuggestionAPIController {
26+
27+
@Value("${aws.accessKeyId:}")
28+
private String accessKey;
29+
30+
@Value("${aws.secretAccessKey:}")
31+
private String secretKey;
32+
33+
@Value("${aws.region:}")
34+
private String region;
35+
36+
@PostMapping
37+
protected String getSuggestedTranslation(Authentication auth, @RequestBody TranslatableText translatableText)
38+
throws IOException, IllegalArgumentException, ResponseStatusException {
39+
if (accessKey.equals("") || secretKey.equals("") || region.equals("")) {
40+
throw new ResponseStatusException(
41+
HttpStatus.INTERNAL_SERVER_ERROR,
42+
"Missing application properties necessary for AWS Translate"
43+
);
44+
} else {
45+
TranslateClient translateClient = buildTranslateClient();
46+
TranslateTextRequest request = buildTranslateTextRequest(translatableText);
47+
return this.translateText(translateClient, request);
48+
}
49+
}
50+
51+
private TranslateClient buildTranslateClient() {
52+
AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);
53+
return TranslateClient.builder()
54+
.region(Region.of(region))
55+
.credentialsProvider(StaticCredentialsProvider.create(credentials))
56+
.build();
57+
}
58+
59+
private TranslateTextRequest buildTranslateTextRequest(TranslatableText translatableText) {
60+
return TranslateTextRequest.builder()
61+
.text(translatableText.getSrcText())
62+
.sourceLanguageCode(translatableText.getSrcLangCode())
63+
.targetLanguageCode(translatableText.getTargetLangCode())
64+
.build();
65+
}
66+
67+
private String translateText(TranslateClient client, TranslateTextRequest request) throws ResponseStatusException {
68+
TranslateTextResponse textResponse;
69+
try {
70+
textResponse = client.translateText(request);
71+
} catch (Exception e) {
72+
throw new ResponseStatusException(
73+
HttpStatus.INTERNAL_SERVER_ERROR,
74+
"Translation failed"
75+
);
76+
}
77+
return textResponse.translatedText();
78+
}
79+
}

src/main/resources/application-dockerdev-sample.properties

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,13 @@ system-wide-salt=secret
216216
#speech-to-text.aws.region=
217217
#speech-to-text.aws.identity-pool-id=
218218

219+
# aws.accessKeyId - [key or leave empty] AWS Translate public key
220+
# aws.secretAccessKey - [key or leave empty] AWS Translate secret key
221+
# aws.region - [region or leave empty] AWS Translate server region
222+
aws.accessKeyId=
223+
aws.secretAccessKey=
224+
aws.region=
225+
219226
# OpenAI and AWS Bedrock Chat endpoints (optional)
220227
#openai.api.key=
221228
#openai.chat.api.url=

src/main/resources/application_sample.properties

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,13 @@ system-wide-salt=secret
216216
#speech-to-text.aws.region=
217217
#speech-to-text.aws.identity-pool-id=
218218

219+
# aws.accessKeyId - [key or leave empty] AWS Translate public key
220+
# aws.secretAccessKey - [key or leave empty] AWS Translate secret key
221+
# aws.region - [region or leave empty] AWS Translate server region
222+
aws.accessKeyId=
223+
aws.secretAccessKey=
224+
aws.region=
225+
219226
# OpenAI and AWS Bedrock Chat endpoints (optional)
220227
#openai.api.key=
221228
#openai.chat.api.url=

src/test/java/org/wise/portal/presentation/web/controllers/author/project/AuthorAPIControllerTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ public void getAuthorProjectConfig_HasProjectRun_ReturnCanGradeStudentWork() thr
8181
expect(appProperties.getProperty("curriculum_base_www"))
8282
.andReturn("http://localhost:8080/curriculum");
8383
expect(appProperties.getProperty("openai.api.key")).andReturn("OPENAPIKEY");
84+
expect(appProperties.getProperty("aws.accessKeyId")).andReturn("ACCESSKEY");
85+
expect(appProperties.getProperty("aws.secretAccessKey")).andReturn("SECRETKEY");
86+
expect(appProperties.getProperty("aws.region")).andReturn("us-west-1");
8487
replay(appProperties);
8588
Map<String, Object> config = authorAPIController.getAuthorProjectConfig(teacherAuth, request,
8689
project1);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.wise.portal.presentation.web.controllers.author.project;
2+
3+
import org.easymock.EasyMockExtension;
4+
import org.easymock.TestSubject;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.ExtendWith;
8+
import org.springframework.test.util.ReflectionTestUtils;
9+
import org.springframework.web.server.ResponseStatusException;
10+
import org.wise.portal.presentation.web.controllers.APIControllerTest;
11+
12+
@ExtendWith(EasyMockExtension.class)
13+
public class TranslationSuggestionAPIControllerTest extends APIControllerTest {
14+
15+
@TestSubject
16+
private final TranslationSuggestionAPIController controller = new TranslationSuggestionAPIController();
17+
18+
@Test
19+
public void getSuggestedTranslation_ThrowIfPropertiesEmpty() throws Exception {
20+
ReflectionTestUtils.setField(controller, "accessKey", "");
21+
ReflectionTestUtils.setField(controller, "secretKey", "");
22+
ReflectionTestUtils.setField(controller, "region", "");
23+
24+
TranslatableText tt = new TranslatableText("English", "Spanish", "text to translate");
25+
26+
assertThrows(ResponseStatusException.class, () -> {
27+
controller.getSuggestedTranslation(teacherAuth, tt);
28+
});
29+
}
30+
}
31+

0 commit comments

Comments
 (0)