From 6da59566d6da2bde7f00bcbf68c6a369c78c7175 Mon Sep 17 00:00:00 2001 From: Nicolas Rocca Date: Fri, 19 Jul 2019 15:58:42 +0100 Subject: [PATCH 1/2] Unbabel Java challenge feature --- pom.xml | 25 +++ .../configuration/UnbabelBeansConfig.java | 35 +++++ .../UnbabelEndpointProperties.java | 23 +++ .../beans/ApiRequestBuilderUtil.java | 67 ++++++++ .../beans/SessionAttributes.java | 12 ++ .../configuration/beans/UnbabelApiBuild.java | 32 ++++ .../challenge/controller/IndexController.java | 50 ------ .../UnbabelTranslationController.java | 75 +++++++++ .../dto/languages/AvailableLanguagesDTO.java | 16 ++ .../challenge/dto/languages/LanguageDTO.java | 13 ++ .../dto/languages/LanguagePairDTO.java | 16 ++ .../dto/languages/SourceLanguageDTO.java | 16 ++ .../dto/languages/TargetLanguageDTO.java | 16 ++ .../translation/TranslationResponseDTO.java | 37 +++++ .../facade/UnbabelTranslationFacade.java | 36 +++++ .../facade/UnbabelTranslationFacadeImpl.java | 82 ++++++++++ .../challenge/facade/data/LanguageData.java | 12 ++ .../facade/data/TranslateFormData.java | 23 +++ .../facade/data/TranslationEntryData.java | 18 +++ .../challenge/model/LanguageModel.java | 41 +++++ .../challenge/model/LanguagePairModel.java | 25 +++ .../com/unbabel/challenge/model/Message.java | 29 ---- .../challenge/model/TranslationModel.java | 34 ++++ ...sitory.java => TranslationRepository.java} | 8 +- .../challenge/service/LanguageService.java | 33 ++++ .../service/LanguageServiceImpl.java | 97 ++++++++++++ .../challenge/service/TranslationService.java | 23 +++ .../service/TranslationServiceImpl.java | 124 +++++++++++++++ src/main/resources/application.properties | 7 + src/main/resources/static/css/main.css | 145 +++++++++++++++++- src/main/resources/static/img/load.svg | 1 + .../resources/static/img/unbabel-logo.svg | 1 + src/main/resources/static/js/main.js | 103 ++++++++++++- .../templates/fragments/newEntryRow.html | 17 ++ .../templates/fragments/targetLanguages.html | 15 ++ src/main/resources/templates/index.html | 69 +++++++-- .../controller/IndexControllerTest.java | 7 - .../UnbabelTranslationControllerTest.java | 59 +++++++ .../facade/UnbabelTranslationFacadeTest.java | 5 + .../repositories/MessageRepositoryTest.java | 17 -- .../TranslationRepositoryTest.java | 76 +++++++++ .../challenge/selenium/FrontEndTest.java | 38 +++++ .../service/LanguageServiceTest.java | 100 ++++++++++++ .../service/TranslationServiceTest.java | 5 + .../challenge/util/UnbabelApiBuildTest.java | 5 + 45 files changed, 1567 insertions(+), 121 deletions(-) create mode 100644 src/main/java/com/unbabel/challenge/configuration/UnbabelBeansConfig.java create mode 100644 src/main/java/com/unbabel/challenge/configuration/UnbabelEndpointProperties.java create mode 100644 src/main/java/com/unbabel/challenge/configuration/beans/ApiRequestBuilderUtil.java create mode 100644 src/main/java/com/unbabel/challenge/configuration/beans/SessionAttributes.java create mode 100644 src/main/java/com/unbabel/challenge/configuration/beans/UnbabelApiBuild.java delete mode 100644 src/main/java/com/unbabel/challenge/controller/IndexController.java create mode 100644 src/main/java/com/unbabel/challenge/controller/UnbabelTranslationController.java create mode 100644 src/main/java/com/unbabel/challenge/dto/languages/AvailableLanguagesDTO.java create mode 100644 src/main/java/com/unbabel/challenge/dto/languages/LanguageDTO.java create mode 100644 src/main/java/com/unbabel/challenge/dto/languages/LanguagePairDTO.java create mode 100644 src/main/java/com/unbabel/challenge/dto/languages/SourceLanguageDTO.java create mode 100644 src/main/java/com/unbabel/challenge/dto/languages/TargetLanguageDTO.java create mode 100644 src/main/java/com/unbabel/challenge/dto/translation/TranslationResponseDTO.java create mode 100644 src/main/java/com/unbabel/challenge/facade/UnbabelTranslationFacade.java create mode 100644 src/main/java/com/unbabel/challenge/facade/UnbabelTranslationFacadeImpl.java create mode 100644 src/main/java/com/unbabel/challenge/facade/data/LanguageData.java create mode 100644 src/main/java/com/unbabel/challenge/facade/data/TranslateFormData.java create mode 100644 src/main/java/com/unbabel/challenge/facade/data/TranslationEntryData.java create mode 100644 src/main/java/com/unbabel/challenge/model/LanguageModel.java create mode 100644 src/main/java/com/unbabel/challenge/model/LanguagePairModel.java delete mode 100644 src/main/java/com/unbabel/challenge/model/Message.java create mode 100644 src/main/java/com/unbabel/challenge/model/TranslationModel.java rename src/main/java/com/unbabel/challenge/repositories/{MessageRepository.java => TranslationRepository.java} (53%) create mode 100644 src/main/java/com/unbabel/challenge/service/LanguageService.java create mode 100644 src/main/java/com/unbabel/challenge/service/LanguageServiceImpl.java create mode 100644 src/main/java/com/unbabel/challenge/service/TranslationService.java create mode 100644 src/main/java/com/unbabel/challenge/service/TranslationServiceImpl.java create mode 100644 src/main/resources/static/img/load.svg create mode 100644 src/main/resources/static/img/unbabel-logo.svg create mode 100644 src/main/resources/templates/fragments/newEntryRow.html create mode 100644 src/main/resources/templates/fragments/targetLanguages.html delete mode 100644 src/test/java/com/unbabel/challenge/controller/IndexControllerTest.java create mode 100644 src/test/java/com/unbabel/challenge/controller/UnbabelTranslationControllerTest.java create mode 100644 src/test/java/com/unbabel/challenge/facade/UnbabelTranslationFacadeTest.java delete mode 100644 src/test/java/com/unbabel/challenge/repositories/MessageRepositoryTest.java create mode 100644 src/test/java/com/unbabel/challenge/repositories/TranslationRepositoryTest.java create mode 100644 src/test/java/com/unbabel/challenge/selenium/FrontEndTest.java create mode 100644 src/test/java/com/unbabel/challenge/service/LanguageServiceTest.java create mode 100644 src/test/java/com/unbabel/challenge/service/TranslationServiceTest.java create mode 100644 src/test/java/com/unbabel/challenge/util/UnbabelApiBuildTest.java diff --git a/pom.xml b/pom.xml index 5f8bf45..345b580 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,31 @@ spring-boot-starter-test test + + org.projectlombok + lombok + 1.18.8 + + + org.modelmapper + modelmapper + 2.3.0 + + + org.fluentlenium + fluentlenium-assertj + 0.10.3 + + + com.github.detro + phantomjsdriver + 1.2.0 + + + xml-apis + xml-apis + 1.4.01 + diff --git a/src/main/java/com/unbabel/challenge/configuration/UnbabelBeansConfig.java b/src/main/java/com/unbabel/challenge/configuration/UnbabelBeansConfig.java new file mode 100644 index 0000000..311ee99 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/configuration/UnbabelBeansConfig.java @@ -0,0 +1,35 @@ +package com.unbabel.challenge.configuration; + +import com.unbabel.challenge.configuration.beans.ApiRequestBuilderUtil; +import com.unbabel.challenge.configuration.beans.SessionAttributes; +import org.modelmapper.ModelMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.WebApplicationContext; + +@Configuration +public class UnbabelBeansConfig +{ + + @Bean + public RestTemplate getRestTemplate(){ + return new RestTemplate(); + } + + @Bean + public ApiRequestBuilderUtil getEndpointUrlBuilderUtil(){return new ApiRequestBuilderUtil();} + + @Bean + public ModelMapper modelMapper() {return new ModelMapper();} + + @Bean + @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) + public SessionAttributes SessionAttributes() { + return new SessionAttributes(); + } + + +} diff --git a/src/main/java/com/unbabel/challenge/configuration/UnbabelEndpointProperties.java b/src/main/java/com/unbabel/challenge/configuration/UnbabelEndpointProperties.java new file mode 100644 index 0000000..5c8e35e --- /dev/null +++ b/src/main/java/com/unbabel/challenge/configuration/UnbabelEndpointProperties.java @@ -0,0 +1,23 @@ +package com.unbabel.challenge.configuration; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties +@ConfigurationProperties("unbabel.endpoint") +@Setter @Getter +public class UnbabelEndpointProperties +{ + private String baseUrl; + private String userNameParamKey; + private String apiKeyParamKey; + private String userNameParamValue; + private String apiKeyParamValue; + private String languagePairApiUrl; + private String translationApiUrl; + +} diff --git a/src/main/java/com/unbabel/challenge/configuration/beans/ApiRequestBuilderUtil.java b/src/main/java/com/unbabel/challenge/configuration/beans/ApiRequestBuilderUtil.java new file mode 100644 index 0000000..ecee57d --- /dev/null +++ b/src/main/java/com/unbabel/challenge/configuration/beans/ApiRequestBuilderUtil.java @@ -0,0 +1,67 @@ +package com.unbabel.challenge.configuration.beans; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.unbabel.challenge.configuration.UnbabelEndpointProperties; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.Arrays; + +@Slf4j +public class ApiRequestBuilderUtil implements UnbabelApiBuild +{ + private final static String AUTH_HEADER_SUFIX = "ApiKey"; + private final static String AUTH_HEADER_NAME = "Authorization"; + private static final String SLASH = "/"; + + @Autowired + UnbabelEndpointProperties endpointProperties; + + + @Override + public String buildLanguagueApiUrl(){ + return buildEndpointUrl(endpointProperties.getLanguagePairApiUrl()).toUriString(); + } + + @Override + public String buildTranslationApiUrl(){ + return buildEndpointUrl(endpointProperties.getTranslationApiUrl()).toUriString(); + } + + @Override + public String addParameterToUrl(final String url, final String parameter){ + return url + parameter; + } + + @Override + public HttpHeaders buildBaseHttpHeader(){ + + HttpHeaders requestHeaders = buildAuthorizattionHttpHeader(); + requestHeaders.setContentType(MediaType.APPLICATION_JSON); + requestHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + + return requestHeaders; + } + + private HttpHeaders buildAuthorizattionHttpHeader(){ + String authorizationHeader = AUTH_HEADER_SUFIX + " " + + endpointProperties.getUserNameParamValue() + ":" + + endpointProperties.getApiKeyParamValue(); + + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.add(AUTH_HEADER_NAME, authorizationHeader); + + return requestHeaders; + } + + protected UriComponentsBuilder buildEndpointUrl(final String apiUrl){ + return UriComponentsBuilder.fromUriString(endpointProperties.getBaseUrl() + apiUrl); + } + + +} diff --git a/src/main/java/com/unbabel/challenge/configuration/beans/SessionAttributes.java b/src/main/java/com/unbabel/challenge/configuration/beans/SessionAttributes.java new file mode 100644 index 0000000..9b94b22 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/configuration/beans/SessionAttributes.java @@ -0,0 +1,12 @@ +package com.unbabel.challenge.configuration.beans; + +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; + +@Getter @Setter +public class SessionAttributes +{ + private HashMap attributes = new HashMap<>(); +} diff --git a/src/main/java/com/unbabel/challenge/configuration/beans/UnbabelApiBuild.java b/src/main/java/com/unbabel/challenge/configuration/beans/UnbabelApiBuild.java new file mode 100644 index 0000000..8097111 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/configuration/beans/UnbabelApiBuild.java @@ -0,0 +1,32 @@ +package com.unbabel.challenge.configuration.beans; + +import org.springframework.http.HttpHeaders; + +public interface UnbabelApiBuild +{ + /** + * Builds url for Unbabel language api + * @return url for unbabel language api + */ + String buildLanguagueApiUrl(); + + /** + * Builds url for Unbabel translation api + * @return url for unbabel translation api + */ + String buildTranslationApiUrl(); + + /** + * Builds Http Header with authorization and media information to consume Unbabel api + * @return HttpHeader with authorization and media information + */ + HttpHeaders buildBaseHttpHeader(); + + /** + * Adds a parameter to a given url + * @param url base url to be modified + * @param parameter string to add to the base url + * @return a url string with the provided url and parameter + */ + String addParameterToUrl(final String url, final String parameter); +} diff --git a/src/main/java/com/unbabel/challenge/controller/IndexController.java b/src/main/java/com/unbabel/challenge/controller/IndexController.java deleted file mode 100644 index 55e7007..0000000 --- a/src/main/java/com/unbabel/challenge/controller/IndexController.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.unbabel.challenge.controller; - -import com.unbabel.challenge.model.Message; -import com.unbabel.challenge.repositories.MessageRepository; -import org.apache.commons.lang.RandomStringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; - -@Controller -public class IndexController { - - @Autowired - MessageRepository messageRepository; - - @Value("${java.challenge.company}") - private String company; // Reads this value from Spring properties file - - /** - * Accepts get requests to the "/" url, generates random messages - * and renders them in thymeleaf template (index.html). - * @param model inject objects into thymeleaf template - * @return generated html page using thymeleaf - */ - @GetMapping("/") - public String main(Model model) { - generateNMessages(10); - //set variables to be used in thymeleaf template - model.addAttribute("company", company); - model.addAttribute("messages", messageRepository.findAll()); - - return "index"; //thymeleaf template name (index -> index.html) - } - - - /** - * Deletes all messages and inserts new random ones - * @param repetitions number of times to loop - */ - private void generateNMessages(int repetitions) { - messageRepository.deleteAll(); //clean all random str - for (int i = 0; i < repetitions; i++) { - messageRepository.save(new Message(RandomStringUtils.randomAlphabetic(30))); - } - } - - -} diff --git a/src/main/java/com/unbabel/challenge/controller/UnbabelTranslationController.java b/src/main/java/com/unbabel/challenge/controller/UnbabelTranslationController.java new file mode 100644 index 0000000..23f3321 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/controller/UnbabelTranslationController.java @@ -0,0 +1,75 @@ +package com.unbabel.challenge.controller; + +import com.unbabel.challenge.facade.data.TranslateFormData; +import com.unbabel.challenge.facade.UnbabelTranslationFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestMethod; + +import javax.validation.Valid; + +/** + * Main controller for home page + */ +@Controller +public class UnbabelTranslationController +{ + private static final String TRANSLATION_ENTRY = "translationEntryRow"; + private static final String FRAG_TARGET_LANGUAGE = "fragments/targetLanguages :: targetLanguagesSelector"; + private static final String FRAG_TRANSLATION_ENTRY = "fragments/newEntryRow :: translationEntry"; + private static final String TARGET_LANGUAGES = "targetLanguages"; + private static final String AVAILABLE_SOURCELANGUAGES = "availableSourcelanguagues"; + private static final String TRANSLATIONS_ENTRIES = "translationsEntries"; + private static final String INDEX_PAGE = "index"; + + @Autowired + private UnbabelTranslationFacade translationFacade; + + /** + * Accepts get requests to the "/" url, generates random messages + * and renders them in thymeleaf template (index.html). + * @param model inject objects into thymeleaf template + * @return generated html home page + */ + @GetMapping("/") + public String main(Model model) { + model.addAttribute(AVAILABLE_SOURCELANGUAGES, translationFacade.getAvailableSourceLanguages()); + model.addAttribute(TRANSLATIONS_ENTRIES, translationFacade.getAllTranslations()); + + return INDEX_PAGE; + } + + /** + * Accepts get request to the url "/targetlanguages/{language}" with the source language code to return + * the compatible target languages + * @param model inject objects into thymeleaf template + * @param languageCode selected source language code + * @return generated html fragment to display target languages + */ + @RequestMapping(value= "/targetlanguages/{language}", method = RequestMethod.GET) + public String getCompatibleTargetLanguages(Model model, @PathVariable("language") String languageCode){ + model.addAttribute(TARGET_LANGUAGES, translationFacade.getCompatibleTargetLanguagues(languageCode)); + + return FRAG_TARGET_LANGUAGE; + } + + /** + * Accepts post request to the url "/translate" to create a translation into Unbabel service + * @param model inject objects into thymeleaf template + * @param formData data in json format with parameters to create a translation + * @return generated html fragment with the information about the created translation + */ + @PostMapping("/translate") + public String processForm(Model model,@Valid @RequestBody TranslateFormData formData) { + model.addAttribute(TRANSLATION_ENTRY, translationFacade.submitTranslation(formData)); + + return FRAG_TRANSLATION_ENTRY; + } + +} diff --git a/src/main/java/com/unbabel/challenge/dto/languages/AvailableLanguagesDTO.java b/src/main/java/com/unbabel/challenge/dto/languages/AvailableLanguagesDTO.java new file mode 100644 index 0000000..3031ff6 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/dto/languages/AvailableLanguagesDTO.java @@ -0,0 +1,16 @@ +package com.unbabel.challenge.dto.languages; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class AvailableLanguagesDTO +{ + @JsonProperty("objects") + private List languages; +} diff --git a/src/main/java/com/unbabel/challenge/dto/languages/LanguageDTO.java b/src/main/java/com/unbabel/challenge/dto/languages/LanguageDTO.java new file mode 100644 index 0000000..16bedcc --- /dev/null +++ b/src/main/java/com/unbabel/challenge/dto/languages/LanguageDTO.java @@ -0,0 +1,13 @@ +package com.unbabel.challenge.dto.languages; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LanguageDTO +{ + @JsonProperty("lang_pair") + private LanguagePairDTO languagePairDTO; +} diff --git a/src/main/java/com/unbabel/challenge/dto/languages/LanguagePairDTO.java b/src/main/java/com/unbabel/challenge/dto/languages/LanguagePairDTO.java new file mode 100644 index 0000000..2566860 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/dto/languages/LanguagePairDTO.java @@ -0,0 +1,16 @@ +package com.unbabel.challenge.dto.languages; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class LanguagePairDTO +{ + @JsonProperty("source_language") + private SourceLanguageDTO sourceLanguage; + + @JsonProperty("target_language") + private TargetLanguageDTO targetLanguage; +} + diff --git a/src/main/java/com/unbabel/challenge/dto/languages/SourceLanguageDTO.java b/src/main/java/com/unbabel/challenge/dto/languages/SourceLanguageDTO.java new file mode 100644 index 0000000..919caaf --- /dev/null +++ b/src/main/java/com/unbabel/challenge/dto/languages/SourceLanguageDTO.java @@ -0,0 +1,16 @@ +package com.unbabel.challenge.dto.languages; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class SourceLanguageDTO +{ + @JsonProperty("name") + private String name; + + @JsonProperty("shortname") + private String shortname; +} diff --git a/src/main/java/com/unbabel/challenge/dto/languages/TargetLanguageDTO.java b/src/main/java/com/unbabel/challenge/dto/languages/TargetLanguageDTO.java new file mode 100644 index 0000000..d61c012 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/dto/languages/TargetLanguageDTO.java @@ -0,0 +1,16 @@ +package com.unbabel.challenge.dto.languages; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TargetLanguageDTO +{ + @JsonProperty("name") + private String name; + + @JsonProperty("shortname") + private String shortname; +} diff --git a/src/main/java/com/unbabel/challenge/dto/translation/TranslationResponseDTO.java b/src/main/java/com/unbabel/challenge/dto/translation/TranslationResponseDTO.java new file mode 100644 index 0000000..d19e02b --- /dev/null +++ b/src/main/java/com/unbabel/challenge/dto/translation/TranslationResponseDTO.java @@ -0,0 +1,37 @@ +package com.unbabel.challenge.dto.translation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class TranslationResponseDTO +{ + @JsonProperty("order_number") + private Double orderNumber; + + @JsonProperty("price") + private Double price; + + @JsonProperty("source_language") + private String sourceLanguage; + + @JsonProperty("status") + private String status; + + @JsonProperty("target_language") + private String targetLanguage; + + @JsonProperty("text") + private String text; + + @JsonProperty("text_format") + private String textFormat; + + @JsonProperty("translatedText") + private String translatedText; + + @JsonProperty("uid") + private String uid; +} diff --git a/src/main/java/com/unbabel/challenge/facade/UnbabelTranslationFacade.java b/src/main/java/com/unbabel/challenge/facade/UnbabelTranslationFacade.java new file mode 100644 index 0000000..672f228 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/facade/UnbabelTranslationFacade.java @@ -0,0 +1,36 @@ +package com.unbabel.challenge.facade; + +import com.unbabel.challenge.facade.data.LanguageData; +import com.unbabel.challenge.facade.data.TranslateFormData; +import com.unbabel.challenge.facade.data.TranslationEntryData; + +import java.util.List; + +public interface UnbabelTranslationFacade +{ + /** + * Retrieve all available source languages from service layer + * @return List of Language Data for use in view layer + */ + List getAvailableSourceLanguages(); + + /** + * Retrieve all available target languages from service layer + * @return List of Language Data for use in view layer + */ + List getCompatibleTargetLanguagues(final String languageCode); + + /** + * Accepts form data submitted by view layer to the service layer in order to create a translation + * @param formData contains the necessary data to create a translation + * @return return information about the created translation entry + */ + TranslationEntryData submitTranslation(final TranslateFormData formData); + + /** + * Gets all translations made by the current user from the service layer + * @return List of translation data, each with information about a translation made by the current user + */ + List getAllTranslations(); + +} diff --git a/src/main/java/com/unbabel/challenge/facade/UnbabelTranslationFacadeImpl.java b/src/main/java/com/unbabel/challenge/facade/UnbabelTranslationFacadeImpl.java new file mode 100644 index 0000000..9d875b5 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/facade/UnbabelTranslationFacadeImpl.java @@ -0,0 +1,82 @@ +package com.unbabel.challenge.facade; + +import com.unbabel.challenge.dto.translation.TranslationResponseDTO; +import com.unbabel.challenge.facade.data.LanguageData; +import com.unbabel.challenge.facade.data.TranslateFormData; +import com.unbabel.challenge.dto.languages.AvailableLanguagesDTO; +import com.unbabel.challenge.dto.languages.SourceLanguageDTO; +import com.unbabel.challenge.dto.languages.TargetLanguageDTO; +import com.unbabel.challenge.facade.data.TranslationEntryData; +import com.unbabel.challenge.service.LanguageService; +import com.unbabel.challenge.service.TranslationService; +import org.modelmapper.ModelMapper; +import org.modelmapper.TypeToken; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +@Component +public class UnbabelTranslationFacadeImpl implements UnbabelTranslationFacade +{ + + @Autowired + LanguageService languageService; + + @Autowired + TranslationService translationService; + + @Autowired + ModelMapper modelMapper; + + @Override + public List getAvailableSourceLanguages(){ + AvailableLanguagesDTO availableLanguagesDTO = languageService.fetchAvailableLanguages(); + List sourceLanguageDTOS = + languageService.filterAllAvailableSourceLanguages(availableLanguagesDTO).stream() + .filter(distinctByKey(SourceLanguageDTO::getName)) + .collect(Collectors.toList()); + Type listType = new TypeToken>() {}.getType(); + + return modelMapper.map(sourceLanguageDTOS, listType); + } + + @Override + public List getCompatibleTargetLanguagues(final String languageCode) + { + AvailableLanguagesDTO availableLanguagesDTO = languageService.fetchAvailableLanguages(); + List targetLanguageDTOS = languageService.findCompatibleTargetLanguages(availableLanguagesDTO, languageCode); + Type listType = new TypeToken>() {}.getType(); + + return modelMapper.map(targetLanguageDTOS, listType); + } + + @Override + public TranslationEntryData submitTranslation(final TranslateFormData formData){ + Optional translationResponseDTO = translationService.createTranslation(formData); + if(translationResponseDTO.isPresent()){ + return modelMapper.map(translationResponseDTO.get(), TranslationEntryData.class); + }else{ + return new TranslationEntryData(); + } + } + + @Override + public List getAllTranslations(){ + Type listType = new TypeToken>() {}.getType(); + return modelMapper.map(translationService.retrieveAllTranslations(), listType); + } + + protected static Predicate distinctByKey(Function keyExtractor) { + Map map = new ConcurrentHashMap<>(); + return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; + } + +} diff --git a/src/main/java/com/unbabel/challenge/facade/data/LanguageData.java b/src/main/java/com/unbabel/challenge/facade/data/LanguageData.java new file mode 100644 index 0000000..1b95754 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/facade/data/LanguageData.java @@ -0,0 +1,12 @@ +package com.unbabel.challenge.facade.data; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class LanguageData +{ + private String name; + private String shortname; +} diff --git a/src/main/java/com/unbabel/challenge/facade/data/TranslateFormData.java b/src/main/java/com/unbabel/challenge/facade/data/TranslateFormData.java new file mode 100644 index 0000000..ace2caa --- /dev/null +++ b/src/main/java/com/unbabel/challenge/facade/data/TranslateFormData.java @@ -0,0 +1,23 @@ +package com.unbabel.challenge.facade.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotBlank; + +@Setter @Getter +public class TranslateFormData +{ + @NotBlank(message = "Text may not be empty") + @JsonProperty("text") + String text; + + @NotBlank(message = "Source Language may not be empty") + @JsonProperty("source_language") + String sourceLanguage; + + @NotBlank(message = "Target Language may not be empty") + @JsonProperty("target_language") + String targetLanguage; +} diff --git a/src/main/java/com/unbabel/challenge/facade/data/TranslationEntryData.java b/src/main/java/com/unbabel/challenge/facade/data/TranslationEntryData.java new file mode 100644 index 0000000..c25ae01 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/facade/data/TranslationEntryData.java @@ -0,0 +1,18 @@ +package com.unbabel.challenge.facade.data; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class TranslationEntryData +{ + private String sourceLanguage; + + private String status; + + private String targetLanguage; + + private String text; + + private String translatedText; +} diff --git a/src/main/java/com/unbabel/challenge/model/LanguageModel.java b/src/main/java/com/unbabel/challenge/model/LanguageModel.java new file mode 100644 index 0000000..8373324 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/model/LanguageModel.java @@ -0,0 +1,41 @@ +package com.unbabel.challenge.model; + +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import java.util.HashSet; +import java.util.Set; + +//@Entity +//@Setter @Getter +public class LanguageModel +{ + /* + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private int id; + + @Column(unique = true, nullable = false) + private String name; + + @Column(unique = true, nullable = false) + private String shortname; + + @ManyToMany() + @JoinTable(name="LANGUAGEPAIRMODEL", joinColumns = {@JoinColumn(name="ID")}, + inverseJoinColumns = {@JoinColumn(name="SOURCELANGUAGEID")}) + private Set sourceLanguage = new HashSet<>(); + + @ManyToMany(mappedBy = "sourceLanguage") + private Set targetLanguage = new HashSet<>(); +*/ + +} diff --git a/src/main/java/com/unbabel/challenge/model/LanguagePairModel.java b/src/main/java/com/unbabel/challenge/model/LanguagePairModel.java new file mode 100644 index 0000000..b8c9149 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/model/LanguagePairModel.java @@ -0,0 +1,25 @@ +package com.unbabel.challenge.model; + +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +//@Entity +//@Setter @Getter +public class LanguagePairModel +{ + /* + @Id + @Column(unique = true, nullable = false) + private int sourceLanguageID; + + @Id + @Column(unique = true, nullable = false) + private int targetLanguageID; +*/ +} + + diff --git a/src/main/java/com/unbabel/challenge/model/Message.java b/src/main/java/com/unbabel/challenge/model/Message.java deleted file mode 100644 index 6139d56..0000000 --- a/src/main/java/com/unbabel/challenge/model/Message.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.unbabel.challenge.model; - -import javax.persistence.*; - -@Entity -public class Message { - - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE) - private Long id; - - @Column(length = 100) - private String msg; - - public Message() { - } - - public Message(String msg) { - this.msg = msg; - } - - public String getMsg() { - return msg; - } - - public void setMsg(String msg) { - this.msg = msg; - } -} diff --git a/src/main/java/com/unbabel/challenge/model/TranslationModel.java b/src/main/java/com/unbabel/challenge/model/TranslationModel.java new file mode 100644 index 0000000..745bf88 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/model/TranslationModel.java @@ -0,0 +1,34 @@ +package com.unbabel.challenge.model; + +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Entity; +import javax.persistence.Id; + +@Entity +@Setter @Getter +public class TranslationModel +{ + @Id + private String uid; + + private Double orderNumber; + + private Double price; + + private String sourceLanguage; + + private String status; + + private String targetLanguage; + + private String text; + + private String textFormat; + + private String translatedText; +} + + + diff --git a/src/main/java/com/unbabel/challenge/repositories/MessageRepository.java b/src/main/java/com/unbabel/challenge/repositories/TranslationRepository.java similarity index 53% rename from src/main/java/com/unbabel/challenge/repositories/MessageRepository.java rename to src/main/java/com/unbabel/challenge/repositories/TranslationRepository.java index 9f3f536..a58dcff 100644 --- a/src/main/java/com/unbabel/challenge/repositories/MessageRepository.java +++ b/src/main/java/com/unbabel/challenge/repositories/TranslationRepository.java @@ -1,10 +1,10 @@ package com.unbabel.challenge.repositories; -import com.unbabel.challenge.model.Message; +import com.unbabel.challenge.model.TranslationModel; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository -public interface MessageRepository extends CrudRepository { - -} \ No newline at end of file +public interface TranslationRepository extends CrudRepository +{ +} diff --git a/src/main/java/com/unbabel/challenge/service/LanguageService.java b/src/main/java/com/unbabel/challenge/service/LanguageService.java new file mode 100644 index 0000000..9d42d8a --- /dev/null +++ b/src/main/java/com/unbabel/challenge/service/LanguageService.java @@ -0,0 +1,33 @@ +package com.unbabel.challenge.service; + +import com.unbabel.challenge.dto.languages.AvailableLanguagesDTO; +import com.unbabel.challenge.dto.languages.SourceLanguageDTO; +import com.unbabel.challenge.dto.languages.TargetLanguageDTO; + +import java.util.List; + +public interface LanguageService +{ + /** + * Send a get request to the Unbabel language api to fetch available languages for translation + * The result is stored on session variable, and always be returned if this one is in session + * to avoid multiple calls to endpoint api + * @return a DTO object containing available languages + */ + AvailableLanguagesDTO fetchAvailableLanguages(); + + /** + * Accepts all available languages and return only the available source languages + * @param availableLanguagesDTO all available languages containing source and target languages + * @return a list of DTO of source languages + */ + List filterAllAvailableSourceLanguages(final AvailableLanguagesDTO availableLanguagesDTO); + + /** + * This will return the target languages compatibles with the given a source language code + * @param availableLanguagesDTO all available languages containing source and target languages + * @param languageCode code of the source language + * @return a list of DTO of target languages + */ + List findCompatibleTargetLanguages(final AvailableLanguagesDTO availableLanguagesDTO, String languageCode); +} diff --git a/src/main/java/com/unbabel/challenge/service/LanguageServiceImpl.java b/src/main/java/com/unbabel/challenge/service/LanguageServiceImpl.java new file mode 100644 index 0000000..6463c4d --- /dev/null +++ b/src/main/java/com/unbabel/challenge/service/LanguageServiceImpl.java @@ -0,0 +1,97 @@ +package com.unbabel.challenge.service; + +import com.unbabel.challenge.configuration.beans.ApiRequestBuilderUtil; +import com.unbabel.challenge.configuration.beans.SessionAttributes; +import com.unbabel.challenge.dto.languages.AvailableLanguagesDTO; +import com.unbabel.challenge.dto.languages.SourceLanguageDTO; +import com.unbabel.challenge.dto.languages.TargetLanguageDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class LanguageServiceImpl implements LanguageService +{ + + private final static String LANG_KEY_ATTRIBUTE = "availableLanguages"; + private final static String ERROR_MSG_LANGUAGE_API = "Could not retrieve available Languages"; + + @Autowired + RestTemplate restTemplate; + + @Autowired + ApiRequestBuilderUtil apiRequestBuilder; + + @Autowired + SessionAttributes sessionAttributes; + + @Override + public AvailableLanguagesDTO fetchAvailableLanguages(){ + + if(sessionAttributes.getAttributes().containsKey(LANG_KEY_ATTRIBUTE)){ + return (AvailableLanguagesDTO) sessionAttributes.getAttributes().get(LANG_KEY_ATTRIBUTE); + }else{ + + try { + HttpEntity requestEntity = new HttpEntity<>(apiRequestBuilder.buildBaseHttpHeader()); + + ResponseEntity responseEntity = restTemplate.exchange( + apiRequestBuilder.buildLanguagueApiUrl(), + HttpMethod.GET, + requestEntity, + AvailableLanguagesDTO.class + ); + + if(responseEntity.getStatusCode() == HttpStatus.OK){ + sessionAttributes.getAttributes().put(LANG_KEY_ATTRIBUTE, responseEntity.getBody()); + return responseEntity.getBody(); + } + + } catch (HttpClientErrorException httpException) { + log.error(ERROR_MSG_LANGUAGE_API,httpException.getStatusCode().toString()); + } catch(Exception generalException) { + log.error(generalException.toString()); + } + } + + return new AvailableLanguagesDTO(); + } + + @Override + public List filterAllAvailableSourceLanguages(final AvailableLanguagesDTO availableLanguagesDTO){ + if(!CollectionUtils.isEmpty(availableLanguagesDTO.getLanguages())){ + return availableLanguagesDTO.getLanguages().stream() + .map(languagePair -> languagePair.getLanguagePairDTO().getSourceLanguage()) + .collect(Collectors.toList()); + }else{ + return Collections.emptyList(); + } + + } + + @Override + public List findCompatibleTargetLanguages(final AvailableLanguagesDTO availableLanguagesDTO, String languageCode){ + if(!CollectionUtils.isEmpty(availableLanguagesDTO.getLanguages())){ + return availableLanguagesDTO.getLanguages().stream() + .filter(languagePair -> languageCode.equals(languagePair.getLanguagePairDTO().getSourceLanguage().getShortname())) + .map(languagePair -> languagePair.getLanguagePairDTO().getTargetLanguage()) + .collect(Collectors.toList()); + }else{ + return Collections.emptyList(); + } + + } + +} diff --git a/src/main/java/com/unbabel/challenge/service/TranslationService.java b/src/main/java/com/unbabel/challenge/service/TranslationService.java new file mode 100644 index 0000000..11ee756 --- /dev/null +++ b/src/main/java/com/unbabel/challenge/service/TranslationService.java @@ -0,0 +1,23 @@ +package com.unbabel.challenge.service; + +import com.unbabel.challenge.dto.translation.TranslationResponseDTO; +import com.unbabel.challenge.facade.data.TranslateFormData; + +import java.util.List; +import java.util.Optional; + +public interface TranslationService +{ + /** + * Sends a post request to Unbabel translation api to create a new translation and caches it to the database + * @param formData contains the necessary data to create a translation request + * @return Optional object containing a DTO with the information of the created translation + */ + Optional createTranslation(final TranslateFormData formData); + + /** + * Gets the cached translations and updates its information from get request from the unbabel translation api + * @return a List of DTO with the updated translations made by current user + */ + List retrieveAllTranslations(); +} diff --git a/src/main/java/com/unbabel/challenge/service/TranslationServiceImpl.java b/src/main/java/com/unbabel/challenge/service/TranslationServiceImpl.java new file mode 100644 index 0000000..004bf8a --- /dev/null +++ b/src/main/java/com/unbabel/challenge/service/TranslationServiceImpl.java @@ -0,0 +1,124 @@ +package com.unbabel.challenge.service; + +import com.unbabel.challenge.facade.data.TranslateFormData; +import com.unbabel.challenge.configuration.beans.ApiRequestBuilderUtil; +import com.unbabel.challenge.dto.translation.TranslationResponseDTO; +import com.unbabel.challenge.model.TranslationModel; +import com.unbabel.challenge.repositories.TranslationRepository; +import lombok.extern.slf4j.Slf4j; +import org.modelmapper.ModelMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +@Service +@Slf4j +public class TranslationServiceImpl implements TranslationService +{ + private static final String ERROR_MSG_TRANSLATION_API = "Call to Translation API returned: "; + + @Autowired + RestTemplate restTemplate; + + @Autowired + ApiRequestBuilderUtil apiRequestBuilder; + + @Autowired + TranslationRepository translationRepository; + + @Autowired + ModelMapper modelMapper; + + @Override + public Optional createTranslation(final TranslateFormData formData){ + + try { + HttpEntity requestEntity = new HttpEntity<>(formData, + apiRequestBuilder.buildBaseHttpHeader()); + + ResponseEntity responseEntity = restTemplate.exchange( + apiRequestBuilder.buildTranslationApiUrl(), + HttpMethod.POST, + requestEntity, + TranslationResponseDTO.class + ); + + if(responseEntity.getStatusCode() == HttpStatus.CREATED){ + TranslationResponseDTO responseDTO = responseEntity.getBody(); + translationRepository.save(modelMapper.map(responseDTO, TranslationModel.class)); + + return Optional.of(responseDTO); + } + + } catch (HttpClientErrorException httpException) { + log.error(ERROR_MSG_TRANSLATION_API,httpException.getStatusCode().toString()); + } catch(Exception generalException) { + log.error(generalException.toString()); + } + + return Optional.empty(); + } + + @Override + public List retrieveAllTranslations(){ + + List allTranslations = Collections.synchronizedList(new ArrayList()); + Iterable translations = translationRepository.findAll(); + Stream translationStream = StreamSupport.stream(translations.spliterator(), Boolean.TRUE); + + translationStream.forEach(translationModel -> { + ResponseEntity responseEntity = sendGETRequestForTranslation(translationModel); + + if(responseEntity.getStatusCode() == HttpStatus.OK){ + updateTranslationModel(responseEntity); + addTranslationEntry(allTranslations, responseEntity.getBody()); + } + }); + + return allTranslations; + } + + private ResponseEntity sendGETRequestForTranslation(final TranslationModel translationModel){ + try { + + HttpEntity requestEntity = new HttpEntity<>(apiRequestBuilder.buildBaseHttpHeader()); + + return restTemplate.exchange( + apiRequestBuilder.addParameterToUrl(apiRequestBuilder.buildTranslationApiUrl(), translationModel.getUid()), + HttpMethod.GET, + requestEntity, + TranslationResponseDTO.class + ); + + } catch (HttpClientErrorException httpException) { + log.error(ERROR_MSG_TRANSLATION_API,httpException.getStatusCode().toString()); + } catch(Exception generalException) { + log.error(generalException.toString()); + } + + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + private void updateTranslationModel(ResponseEntity responseEntity){ + TranslationResponseDTO responseDTO = responseEntity.getBody(); + TranslationModel translationModel = modelMapper.map(responseDTO, TranslationModel.class); + translationRepository.save(translationModel); + } + + synchronized void addTranslationEntry(List allTranslations, TranslationResponseDTO responseDTO){ + allTranslations.add(responseDTO); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6613981..fa2267a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,9 @@ java.challenge.company=Unbabel +unbabel.endpoint.baseUrl=https://sandbox.unbabel.com/tapi/v2 +unbabel.endpoint.languagePairApiUrl=/language_pair/ +unbabel.endpoint.translationApiUrl=/translation/ +unbabel.endpoint.userNameParamKey=username +unbabel.endpoint.apiKeyParamKey=api_key +unbabel.endpoint.userNameParamValue=fullstack-challenge +unbabel.endpoint.apiKeyParamValue=9db71b322d43a6ac0f681784ebdcc6409bb83359 spring.thymeleaf.cache=false diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css index ebd0a40..ed2865d 100644 --- a/src/main/resources/static/css/main.css +++ b/src/main/resources/static/css/main.css @@ -1,3 +1,146 @@ .company-name{ margin:15px 0 20px 0; -} \ No newline at end of file +} + +.company-name{ + margin:15px 0 20px 0; +} + +.unbabel .navbar{ + background-color:#3041d9; + padding:20px +} + +.unbabel .navbar .logo-slogan{ + color:white; + font-size: 1.25rem; + font-weight: 300; + margin-left:5px; + position:relative; + top:4px; +} + +.unbabel .translation-box{ + margin-top:100px; + margin-bottom:50px; + padding-left:20px; + padding-right:20px; + width:100%; +} + +.unbabel .translation-box .header{ + display: flex; + height:60px; + padding:20px; + color:white; + background-color:#15006d; + border-radius:15px 15px 0 0; +} + +.unbabel #select-targetlanguage-block{ + margin-left: 10px; +} + +.unbabel .translation-box #textarea{ + width: 100%; + position: relative; + top: 0px; + border-radius: 0 0 15px 15px; + border: solid #15006d; + padding: 10px 15px 10px 15px; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + outline:none !important; +} + +.unbabel .translation-box #textarea:focus{ + background-color:#c6d2ff; +} + +.unbabel .translate-btn { + width:300px; + margin-top:20px; + padding: 0 32px; + cursor: pointer; + transition: background-color .15s ease; + text-decoration: none; + font-family: Montserrat,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif; + color: #fff; + border: 0; + border-radius: 15px; + outline: 0; + background-color: #3953bf; + text-shadow: 0 1px 0 rgba(0,0,0,.1); + font-weight: 700; + line-height: 48px; + position: relative; +} + +.unbabel .translate-btn:hover{ + color: #fff; + background-color: #859eff; +} + +.unbabel .translation-history{ + margin-top: 30px; + margin-bottom: 70px; + padding-left: 20px; + padding-right: 20px; + width: 100%; +} + +.unbabel .table-wrapper{ + border: solid #15006d; + border-radius: 15px; +} + +.unbabel .translation-history-table{ + width: 100%; + border-collapse: collapse; + border-radius: 10px; + overflow: hidden; +} + +.unbabel .translation-history-table th, +.unbabel .translation-history-table td { + padding:15px; +} + +.unbabel .translation-history-table th{ + color:white; + background-color:#15006d; +} + +.unbabel .translation-history-table tr:nth-child(odd) { + background-color: #c6d2ff; +} + +.unbabel #submit-translation .load-icon{ + width:40px; +} + +textarea::-webkit-input-placeholder { + font-weight:600; + color:#adb5bd; +} + +textarea:-moz-placeholder { /* Firefox 18- */ + font-weight:600; + color:#adb5bd; +} + +textarea::-moz-placeholder { /* Firefox 19+ */ + cfont-weight:600; + color:#adb5bd; +} + +textarea:-ms-input-placeholder { + font-weight:600; + color:#adb5bd; +} + +textarea::placeholder { + font-weight:600; + color:#adb5bd; +} diff --git a/src/main/resources/static/img/load.svg b/src/main/resources/static/img/load.svg new file mode 100644 index 0000000..eea0e9b --- /dev/null +++ b/src/main/resources/static/img/load.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/img/unbabel-logo.svg b/src/main/resources/static/img/unbabel-logo.svg new file mode 100644 index 0000000..589f19e --- /dev/null +++ b/src/main/resources/static/img/unbabel-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/static/js/main.js b/src/main/resources/static/js/main.js index fec41f1..dde058b 100644 --- a/src/main/resources/static/js/main.js +++ b/src/main/resources/static/js/main.js @@ -1 +1,102 @@ -console.log("Test:","The main.js file is loaded!"); \ No newline at end of file +console.log("Test:","The main.js file is loaded!"); + +var TMOD = { + + config: { + targetLanguagesUrl: "http://localhost:8080/targetlanguages/", + translateUrl : "http://localhost:8080/translate/", + sourceLangSelector: "#source_lang", + targetLanguagesBlock: "#select-targetlanguage-block", + formFieldSelector : ".form-field", + id:"id", + textAreaSelector : "#textarea", + submitButton : "#submit-translation", + translationFormId : "", + maxFields: 3, + translationEntryId: "#new-entry-row", + loadIconSelector: "#submit-translation .load-icon", + textButtonSelector: "#submit-translation span", + hideClass : "d-none", + empty: "" + }, + + showTargetLanguages: function (){ + $(this.config.sourceLangSelector).change(function() { + if($(this).val() !== TMOD.config.empty){ + $.ajax({ + type: "get", + url: TMOD.config.targetLanguagesUrl + $(this).val(), + dataType : 'html', + success : function(response){ + $(TMOD.config.targetLanguagesBlock).html($.parseHTML(response)); + }, + error : function(){ + $(TMOD.config.sourceLangSelector).val(TMOD.config.empty); + } + }); + } + }); + }, + + sendPostRequest: function(fieldsSelector){ + + var formData = {} + $(fieldsSelector).each(function(){ + formData[$(this).data(TMOD.config.id)] = $(this).val(); + }); + $.ajax({ + type: "post", + url: TMOD.config.translateUrl, + contentType : "application/json", + dataType : 'html', + data : JSON.stringify(formData), + success : function(response){ + $(TMOD.config.textAreaSelector).val(""); + $(TMOD.config.translationEntryId).after($.parseHTML(response)); + alert("Translation submitted with success"); + $(TMOD.config.loadIconSelector).addClass(TMOD.config.hideClass); + $(TMOD.config.textButtonSelector).removeClass(TMOD.config.hideClass); + $(TMOD.config.submitButton).prop("disabled", false); + }, + error : function(){ + alert("Something went wrong, please submit again"); + $(TMOD.config.loadIconSelector).addClass(TMOD.config.hideClass); + $(TMOD.config.textButtonSelector).removeClass(TMOD.config.hideClass); + $(TMOD.config.submitButton).prop("disabled", false); + } + }); + }, + + submitTranslation: function(){ + $(TMOD.config.submitButton).click( function() { + + var fieldCounter = 0; + $(TMOD.config.formFieldSelector).each(function() { + if($.trim($(this).val()) !== TMOD.config.empty){ + fieldCounter++; + } + }); + + if(TMOD.config.maxFields === fieldCounter){ + $(TMOD.config.loadIconSelector).removeClass(TMOD.config.hideClass); + $(TMOD.config.textButtonSelector).addClass(TMOD.config.hideClass); + $(TMOD.config.submitButton).prop("disabled", true); + + TMOD.sendPostRequest(TMOD.config.formFieldSelector); + }else{ + alert("All fields are mandatory"); + } + + }); + }, + + init: function(){ + $(document).ready( function() { + TMOD.showTargetLanguages(); + TMOD.submitTranslation(); + }); + } + +}; + +TMOD.init(); \ No newline at end of file diff --git a/src/main/resources/templates/fragments/newEntryRow.html b/src/main/resources/templates/fragments/newEntryRow.html new file mode 100644 index 0000000..1184fa2 --- /dev/null +++ b/src/main/resources/templates/fragments/newEntryRow.html @@ -0,0 +1,17 @@ + + + + + +
+ + + + + + + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/targetLanguages.html b/src/main/resources/templates/fragments/targetLanguages.html new file mode 100644 index 0000000..c99ace7 --- /dev/null +++ b/src/main/resources/templates/fragments/targetLanguages.html @@ -0,0 +1,15 @@ + + + + + +
+ to: + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index d884594..a63960f 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -10,25 +10,70 @@ - + + +