Problem
Make a translator that uses the services of online translation, this translator must then be published using both as JAX-WS as well as JAX-RS
Solution
To solve this problem we will use Web service Code first (Bottom up) approach.
The online translators that will be used are: Google Translator, Microsoft Translator, Systran Translator
The libs that will be used are: Spring Framework 4.0.0, Apache CXF 3.0.0, HttpClient 4.3.2
Structure
We will follow the next structure:
-
Core: It contains the main logic that allows us to make the translation, it is formed by:
1.1. Application Services These are the direct clients of the domain model. They remain very lightweight, coordinating operations performed against domain objects.
1.2. Domain This is where the business logic and rules of an application live, and it is the heart of the software.
1.3. Infrastructure This is where general technical, plumbing - related code happens. This layer acts as a supporting library for all the other layers
-
Interface: This include the classes needed to be able to publish the services using both as JAX-WS as well as JAX-RS
1. Core
1.1 Application Services:
Main interface
public interface TranslatorService {
TranslatedText translate(String langFrom, String langTo, String text);
}
Main implementation
@Service
public class TranslatorServiceImpl implements TranslatorService {
@Autowired
Translator googleTranslator;
@Autowired
Translator microsoftTranslator;
@Autowired
Translator systranTranslator;
public TranslatedText translate(String langFrom, String langTo, String text) {
LanguageSourceTarget languageSourceTarget = new LanguageSourceTarget(Language.fromString(langFrom), Language.fromString(langTo));
if (languageSourceTarget.sourceAndTargeAreEquals()) {
throw new TranslatorException("The languages from and to must be differents.");
}
Future<String> googleResult = googleTranslator.translate(languageSourceTarget, text);
Future<String> systranResult = systranTranslator.translate(languageSourceTarget, text);
Future<String> microsoftResult = microsoftTranslator.translate(languageSourceTarget, text);
TranslatedText response = new TranslatedText();
response.setFrom(languageSourceTarget.getSourceAsStr());
response.setTo(languageSourceTarget.getTargetAsStr());
response.setMicrosoftTranslation(getTranslation(microsoftResult));
response.setGoogleTranslation(getTranslation(googleResult));
response.setSystranTranslation(getTranslation(systranResult));
return response;
}
Notes:
- In this method the 3 translators are called.
Future
is used in order to execute the translators asynchronously- The return class TranslatedText has the 3 translations
1.2. Domain
Main Interface
public interface Translator {
public Future<String> translate(LanguageSourceTarget languageSourceTarget, String text);
public String detectLanguage(String text);
}
The pair of languages to be used in translation, is represented by:
public class LanguageSourceTarget {
private Language source;
private Language target;
The language to be used in translation, is represented by:
public enum Language {
AFRIKAANS("af"),
ALBANIAN("sq"),
...
The result of the translation is represented by:
public class TranslatedText {
private String from;
private String to;
private String googleTranslation;
private String microsoftTranslation;
private String systranTranslation;
1.3. Infrastructure
Base Implementation of the translators (Template Method)
public abstract class TranslatorImpl implements Translator {
@Async
public Future<String> translate(LanguageSourceTarget languageSourceTarget, String text) {
try {
String encodedText = URLEncoder.encode(text, ENCODING_UTF_8);
String from = languageSourceTarget.getSource().asStr();
String to = languageSourceTarget.getTarget().asStr();
return new AsyncResult(translateInternal(from, to, text, encodedText));
} catch (IOException e) {
LOG.error("Problems translating:" + e.getMessage(), e);
throw new TranslatorException("Problems translating:" + e.getMessage(), e);
}
}
protected String translateInternal(String from, String to, String text, String encodedText) throws IOException {
HttpRequestBase requestBase = getHttpRequest(from, to, text, encodedText);
HttpClient httpclient = HttpClientBuilder.create().build();
HttpResponse response = httpclient.execute(requestBase);
HttpEntity responseEntity = response.getEntity();
String responseAsStr = transformToString(responseEntity);
if (StringUtils.hasText(responseAsStr)) {
return getTranslationFrom(responseAsStr);
}
return "";
}
protected abstract HttpRequestBase getHttpRequest(String from, String to, String text, String encodedText);
protected abstract String getTranslationFrom(String responseAsStr);
Main methods:
translate:
- Encode the text using UTF-8
- Get the string representations of the languages
- Call to translate internal
- It is market as Async to mark this method as asynchronous
translateInternal:
- Get the HttpRequest (GET or POST) to be used.
- The HttpRequest is invoked using HttpClient, with this we can get the HttpResponse that contains the Entity, which is the response of the translator
- Convert the Entity to String.
- Parsing this string we can get the translation
Abstract Methods:
The following methods must be overwritten by each implementation of the translator
HttpRequestBase getHttpRequest(String from, String to, String text, String encodedText)
Return a HttpRequest object that contains: the method (GET or POST), the url that must be called and the parameters that must be sent.
String getTranslationFrom(String responseAsStr)
Receive a String representing the full response of the online translator, and parsing it returns the translation
Google Translator
@Component("googleTranslator")
public class GoogleTranslator extends TranslatorImpl {
@Override
protected HttpRequestBase getHttpRequest(String from, String to, String text, String encodedText) {
HttpGet httpGet = new HttpGet("http://translate.google.com/translate_a/t?client=t&text=" + encodedText + "&hl=" + from + "&sl=" + from + "&tl=" + to + "&multires=1&otf=2&ssel=0&tsel=0&sc=1&ie=" + ENCODING_UTF_8);
return httpGet;
}
protected String getTranslationFrom(String responseAsStr) {
StringBuilder sb = new StringBuilder();
if (responseAsStr.length() > 4) {
int idxEnd = responseAsStr.indexOf("\"", 4);
sb.append(responseAsStr.substring(4, idxEnd));
}
return sb.toString();
}
Text to translate: "This is a test" (From en to es)
http://translate.google.com/translate_a/t?client=t&text=This+is+a+test&hl=en&sl=en&tl=es&multires=1&otf=2&ssel=0&tsel=0&sc=1&ie=UTF-8
[[["Esta es una prueba","This is a test","",""]],,"en",,[["Esta es una prueba",[1],true,false,390,0,4,0]],[["This is a test",1,[["Esta es una prueba",390,true,false],["Esto es una prueba",31,true,false],["esta es una prueba",0,true,false],["Es una prueba",0,true,false],["Este es un examen",0,true,false]],[[0,14]],"This is a test"]],,,[["en"]],103]
Microsoft Translator
@Component("microsoftTranslator")
public class MicrosoftTranslator extends TranslatorImpl {
private static String API_KEY = "YOUR_API_KEY";
@Override
protected HttpRequestBase getHttpRequest(String from, String to, String text, String encodedText) {
String urlStr = "http://api.microsofttranslator.com/v2/Http.svc/Translate";
String PARAMETERS = "appId=" + API_KEY + "&text=" + encodedText + "&from=" + from + "&to=" + to + "";
HttpGet httpget = new HttpGet(urlStr + "?" + PARAMETERS);
return httpget;
}
@Override
protected String getTranslationFrom(String responseAsStr) {
return getResultFromResponseStr(responseAsStr);
}
public String detectLanguage(String text) {
try {
String encodedText = URLEncoder.encode(text, ENCODING_UTF_8);
String urlStr = "http://api.microsofttranslator.com/v2/Http.svc/Detect";
String parameters = "appId=" + API_KEY + "&text=" + encodedText;
HttpGet httpget = new HttpGet(urlStr + "?" + parameters);
HttpClient httpclient = HttpClientBuilder.create().build();
HttpResponse response = httpclient.execute(httpget);
HttpEntity entity = response.getEntity();
String responseStr = transformToString(entity);
return getResultFromResponseStr(responseStr);
} catch (Throwable e) {
LOG.error("Problems detecting language:" + e.getMessage(), e);
throw new TranslatorException("Problems detecting language:" + e.getMessage(), e);
}
}
private static String getResultFromResponseStr(String responseAsStr) {
if (!StringUtils.hasText(responseAsStr)) {
return "";
}
int idxBegin = responseAsStr.indexOf(">");
int idxEnd = responseAsStr.indexOf("<", idxBegin + 1);
return responseAsStr.substring(idxBegin + 1, idxEnd);
}
Text to translate: "This is a test" (From en to es)
http://api.microsofttranslator.com/v2/Http.svc/Translate?appId=YOUR_API_KEY&text=This+is+a+test&from=en&to=es
<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">Esto es una prueba</string>
Systran Translator
@Component("systranTranslator")
public class SystranTranslator extends TranslatorImpl {
@Override
protected HttpRequestBase getHttpRequest(String from, String to, String text, String encodedText) {
String lpStr = from + "_" + to;
String urlStr = "http://www.systranet.com/sai?gui=text&lp=" + lpStr + "&sessionid=13071170317011544&service=urlmarkuptranslate";
text = "<html><body>" + text + "<br></body></html>";
StringEntity entityStr = new StringEntity(text, ENCODING_UTF_8);
HttpPost httpPost = new HttpPost(urlStr);
httpPost.setEntity(entityStr);
return httpPost;
}
@Override
protected String getTranslationFrom(String responseAsStr) {
String classResult = "<html>";
int idxBegin = responseAsStr.indexOf(classResult);
idxBegin = responseAsStr.indexOf(classResult, idxBegin + 1);
int idxEnd = responseAsStr.length() - 1;
String htmlResult = responseAsStr.substring(idxBegin, idxEnd);
String result = SimpleHtmlParser.getInnerText(htmlResult);
return result != null ? result.trim() : "";
}
Text to translate: "This is a test" (From en to es)
http://www.systranet.com/sai?gui=text&lp=en_es&sessionid=13071170317011544&service=urlmarkuptranslate
<html><body><span class="systran_seg" id="Sp1.s2_o"><span class="systran_token_word" value="2428/pron" id="p1.t2_1">This</span> <span class="systran_token_word" value="4004/verb:plain" id="p1.t2_2">is</span> <span class="systran_token_word" value="3c3e/det" id="p1.t2_3">a</span> <span class="systran_altmeaning" value="<\;reference>\;test7f7f<\;/reference>\;<\;choice value='altmeaning-,-31322,-,100'>\;[criterio normal] (SYSTRAN Alternative Dictionary)<\;/choice>\;<\;choice value='altmeaning-,-31323,-,100'>\;[ensayo] (SYSTRAN Alternative Dictionary)<\;/choice>\;<\;choice value='altmeaning-,-31324,-,100'>\;[examen] (SYSTRAN Alternative Dictionary)<\;/choice>\;<\;choice value='_main,0' default='yes'>\;[prueba] (General)<\;/choice>\;<\;source>\;test<\;/source>\;" id="altmeaning_1"><span class="systran_token_word" value="1010/noun:common" id="p1.t2_4">test</span></span></span><br></body></html>;<html>
<meta http-equiv="Content-Type" content="text/html\; charset=UTF-8">
<body><span class="systran_seg" id="Sp1.s2_o"><span class="systran_token_word" value="2428/pron" id="p1.t2_1">Esto</span> <span class="systran_token_word" value="4004/verb:plain" id="p1.t2_2">es</span> <span class="systran_token_word" value="*" id="p1.t2_0">una</span> <span class="systran_altmeaning" value="<\;reference>\;test7f7f<\;/reference>\;<\;choice value='altmeaning-,-31322,-,100'>\;[criterio normal] (SYSTRAN Alternative Dictionary)<\;/choice>\;<\;choice value='altmeaning-,-31323,-,100'>\;[ensayo] (SYSTRAN Alternative Dictionary)<\;/choice>\;<\;choice value='altmeaning-,-31324,-,100'>\;[examen] (SYSTRAN Alternative Dictionary)<\;/choice>\;<\;choice value='_main,0' default='yes'>\;[prueba] (General)<\;/choice>\;<\;source>\;test<\;/source>\;" id="altmeaning_1"><span class="systran_token_word" value="1010/noun:common" id="p1.t2_4">prueba</span></span></span><br></body></html>;
Testing Translation Service
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"/META-INF/spring/applicationContext.xml"})
public class TranslatorServiceTest {
@Autowired
TranslatorService translatorService;
@Test
public void translateTest() throws Exception {
TranslatedText translatedText = translatorService.translate("en", "es", "This is a test of translation service");
System.out.println(translatedText);
}
It will print:
TranslatedText{from='en', to='es', googleTranslation='Esta es una prueba de servicio de traducción', microsoftTranslation='Esta es una prueba de servicio de traducción', systranTranslation='Ésta es una prueba del servicio de traducción'}
2. Interface
2.1 JAX-RS
@Path("/")
public interface TranslatorRest {
@GET
@Path("translate/{from}/{to}/{text}")
@Produces({MediaType.APPLICATION_JSON})
public TranslatedText translate(@PathParam("from") String from, @PathParam("to") String to, @PathParam("text") String text);
}
Main Implementation
@Service
public class TranslatorRestImpl implements TranslatorRest {
@Autowired
TranslatorService translatorService;
public TranslatedText translate(String from, String to, String text) {
TranslatedText translatedText = translatorService.translate(from, to, text);
return translatedText;
}
}
Notes:
- It is only required to put the JAX-RS annotations in the interface
- The main method only calls the application service. There is no business logic in these methods
Exception Handler
@Component
public class ExceptionHandler implements ExceptionMapper {
public javax.ws.rs.core.Response toResponse(Throwable throwable) {
return Response.serverError().entity(throwable.getMessage()).build();
}
}
Examples:
Request:
http://services.anotes.org/translator/translate/en/es/task
Response:
{"from":"en","to":"es","googleTranslation":"tarea","microsoftTranslation":"tarea","systranTranslation":"tarea"}
Using filters
We need that this service can support links as the following:
http://services.anotes.org/translator/translate/task
In this there is not any information about the language neither "from" nor "to" but we can infer both of them and convert this link to:
http://services.anotes.org/translator/translate/en/es/task
To infer the "from" language we can use the detectLanguage
method of MicrosoftTranslator using the text to translate as parameter
To infer the "to" language we can use the language sent in the request header.
To convert the url before calling the TranslatorRest we must use a filter. For this case we will implement ContainerRequestFilter as show below
Container Request Filter
@Component
@Provider
@PreMatching
public class LanguageSetterFilter implements ContainerRequestFilter {
private static final String DEFAULT_LANG_TO = "es";
private static final String PATH_TRANSLATE = "/translate/";
@Autowired
Translator microsoftTranslator;
public void filter(ContainerRequestContext ctx) {
UriInfo uriInfo = ctx.getUriInfo();
String uriStr = uriInfo.getRequestUri().toString();
int idx = uriStr.indexOf(PATH_TRANSLATE);
boolean isTranslatePath = idx != -1;
if (isTranslatePath) {
String parameters = uriStr.substring(idx + PATH_TRANSLATE.length());
boolean existLanguages = parameters.indexOf("/") != -1;
if (!existLanguages) {
String headerLanguage = getHeaderLanguage(ctx);
ctx.setRequestUri(getNewUri(uriStr, idx, parameters, headerLanguage));
}
}
}
private String getHeaderLanguage(ContainerRequestContext ctx) {
String headerString = ctx.getHeaderString("accept-language");
return headerString != null && headerString.length() > 2 ? headerString.substring(0, 2) : DEFAULT_LANG_TO;
}
private URI getNewUri(String uriStr, int idx, String parameters, String headerLanguage) {
String langFrom = microsoftTranslator.detectLanguage(parameters);
String langTo = headerLanguage;
String newUri = uriStr.substring(0, idx + PATH_TRANSLATE.length()) + langFrom + "/" + langTo + "/" + parameters;
try {
return new URI(newUri);
} catch (URISyntaxException e) {
LOG.error("Getting new uri:" + newUri, e);
throw new TranslatorException("Problems Getting new uri:" + newUri, e);
}
}
}
Method
filter:
- Check if the url contains "translate"
- Check if exist the languages if not exist, both will be infered; the first using Microsoft Translator
public String detectLanguage(String text)
method; and the "to" from the request header. - Then a new url is form and this contains the from and to language. This will be processed by TranslatorRest
JAX-RS Bean Declaration
<jaxrs:server address="/translator">
<jaxrs:serviceBeans>
<ref bean="translatorRestImpl"/>
</jaxrs:serviceBeans>
<jaxrs:providers>
<ref bean="languageSetterFilter"/>
<ref bean="exceptionHandler"/>
<bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider"/>
</jaxrs:providers>
</jaxrs:server>
Wadl
<application>
<grammars/>
<resources base="http://services.gamal-mateo.cloudbees.net/translator">
<resource path="/">
<resource path="translate/{from}/{to}/{text}">
<param name="from" style="template" type="xs:string"/>
<param name="to" style="template" type="xs:string"/>
<param name="text" style="template" type="xs:string"/>
<method name="GET">
<request/>
<response>
<representation mediaType="application/json"/>
</response>
</method>
</resource>
</resource>
</resources>
</application>
Testing
public class TranslatorRestTest {
private final static String ENDPOINT_ADDRESS = "http://localhost:8062/apache-cxf-example/translator";
public static final String ENCODING_UTF_8 = "UTF-8";
@Test
public void translateTest() throws IOException {
Client client = ClientBuilder.newClient();
Response response = client.target(ENDPOINT_ADDRESS + "/translate/es/en/prueba").request("application/json").get();
String responseStr = getResponseAsStr(response);
System.out.println(responseStr);
}
It will print:
{"from":"es","to":"en","googleTranslation":"test","microsoftTranslation":"","systranTranslation":"test"}
2.1 JAX-WS
Main Interface
@WebService
public interface TranslatorPT {
@SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE)
@WebMethod(action = "http://anotes.org/services/translator/ws/translate")
@WebResult(name = "TranslateResponse", targetNamespace = "http://anotes.org/services/translator/ws/schema", partName = "part")
@ResponseWrapper(targetNamespace = "http://anotes.org/services/translator/ws/schema", className = "org.anotes.services.translator.ws.schema.TranslateResponse")
@RequestWrapper(targetNamespace = "http://anotes.org/services/translator/ws/schema", className = "org.anotes.services.translator.ws.schema.TranslateRequest")
public TranslateResponse translate(@WebParam(name = "translateRequest",
partName = "part",
targetNamespace = "http://anotes.org/services/translator/ws/schema")
TranslateRequest request);
}
Main Implementation
@Service
@WebService(portName = "translatorPort",
serviceName = "translatorService",
targetNamespace = "http://anotes.org/services/translator/ws",
endpointInterface = "org.anotes.services.translator.ws.TranslatorPT")
public class TranslatorPTImpl implements TranslatorPT {
@Autowired
TranslatorService translatorService;
@Autowired
ConversionService conversionService;
public TranslateResponse translate(TranslateRequest request) {
TranslatedText translatedText = translatorService.translate(request.getLangFrom(), request.getLangTo(), request.getText());
TranslateResponse response = conversionService.convert(translatedText, TranslateResponse.class);
return response;
}
}
Notes:
- It is only required to put the full JAX-WS annotations in the interface methods
- It is required to put the full class JAX-WS @WebService annotations in the implementation class
- The main method only calls the application service. There is no business logic in these methods
Request
public class TranslateRequest {
private String langFrom;
private String langTo;
private String text;
public class TranslateResponse {
private ResultEnum resultCode = ResultEnum.OK;
private String errorMsg;
private String googleTranslation;
private String microsoftTranslation;
private String systranTranslation;
package-info It is needed in order to point where will be generated the objects for this web service
@javax.xml.bind.annotation.XmlSchema(namespace = "http://anotes.org/services/translator/ws/schema")
package org.anotes.services.translator.ws.schema;
Exception Handler
We will use spring-aop; specifically we will use the following aspect
in order to intercept all calls to public methods of the *PTImpl class;
and in case of exception we will generate an object of TranslateResponse
setting the result code and the error message
@Aspect
@Component
public class WsServicesAspect {
protected final Logger LOG = LoggerFactory.getLogger(getClass());
@Pointcut("bean(*PTImpl)")
private void serviceBean() {
}
@Pointcut("execution(public * *(..))")
private void publicMethod() {
}
@Around("serviceBean() && publicMethod()")
public Object processServicePtPublicMethods(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
String method = proceedingJoinPoint.getSignature().toShortString();
try {
LOG.info("Around: Before executing:" + method);
Object obj = proceedingJoinPoint.proceed();
LOG.info("Around: After executing:" + method);
return obj;
} catch (Throwable throwable) {
LOG.error("Problems calling the method: " + method, throwable);
String errorMsg = throwable.getMessage();
Object responseObj = getResponseInstance(proceedingJoinPoint);
BeanWrapper beanWrapper = new BeanWrapperImpl(responseObj);
beanWrapper.setPropertyValue("resultCode", ResultEnum.ERROR);
beanWrapper.setPropertyValue("errorMsg", errorMsg);
return responseObj;
}
}
JAX-WS Bean Declaration
<!-- JAX-WS -->
<jaxws:endpoint address="/translatorPort"
implementor="#translatorPTImpl"/>
Wsdl
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:tns="http://anotes.org/services/translator/ws"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:ns1="http://ws.translator.services.anotes.org/" name="translatorService"
targetNamespace="http://anotes.org/services/translator/ws">
<wsdl:import location="http://services.gamal-mateo.cloudbees.net/translatorPort?wsdl=TranslatorPT.wsdl"
namespace="http://ws.translator.services.anotes.org/"></wsdl:import>
<wsdl:binding name="translatorServiceSoapBinding" type="ns1:TranslatorPT">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="translate">
<soap:operation soapAction="http://anotes.org/services/translator/ws/translate" style="document"/>
<wsdl:input name="translate">
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output name="translateResponse">
<soap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="translatorService">
<wsdl:port binding="tns:translatorServiceSoapBinding" name="translatorPort">
<soap:address location="http://services.gamal-mateo.cloudbees.net/translatorPort"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
Testing
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"/META-INF/spring/spring-context-test.xml"})
public class TranslatorPTTest {
@Autowired
private TranslatorPT wsClient;
@Test
public void translateTest() {
TranslateRequest request = new TranslateRequest();
request.setLangFrom("es");
request.setLangTo("en");
request.setText("Esta es una prueba de JAXWS");
TranslateResponse response = wsClient.translate(request);
System.out.println(response);
}
It will print:
TranslateResponse{resultCode=OK, errorMsg='null', googleTranslation='This is a test of JAXWS', microsoftTranslation='', systranTranslation='This is a test of JAXWS'}
Final Notes:
-
Microsoft translator needs an API KEY, so you have to request one and then put it on the
private static String API_KEY = "YOUR_API_KEY"
in MicrosoftTranslator -
Gradle is used so you can run the application executing: gradle jettyrun
10 comments:
Awesome Work ! Thanks a ton :D
I have lots of confusion as there are many different library/implementations are provided by different vendors for developing the webServices. like implementation provided by SUN (metro, Jersey) and by apache (axis 1, soap, axis 2, cxf). I am new to web services so could you provide some guidelines how do I start with to learn about it. Thanks
Thanks for your comment.
I think that you must start checking the cxf because is easy and support both jax-ws and jax-rs
Pretty good post. I just stumbled upon your blog and wanted to say that I have really enjoyed reading your blog posts. I hope you post again soon. Big thanks for the useful info. online translation agency in USA
Really very happy to mention , your post is extremely interesting to read. I never stop myself to mention something about translation services. Haiti
Really very happy to say, your post is very interesting. I never stop myself from saying something about it. You’re doing a great job. Keep it up. certified translation services in riyadh
Thank you for sharing such a Magnificent post here. I found this blog very useful for future references. keep sharing such informative blogs with us. Translation Services USA
Excellent info, the information which you have provided is very informative and necessary for everyone. Always keep sharing this kind of information. Thank you.Advanced Transcription service in Gurgaon
英語論文翻訳 Wow, cool post. I'd like to write like this too - taking time and real hard work to make a great article... but I put things off too much and never seem to get started. Thanks though.
I generally check this kind of article and I found your article which is related to my interest. Genuinely it is good and instructive information. Portuguese Innovative Language Learning Online App Thankful to you for sharing an article like this.
I generally check this kind of article and I found your article which is related to my interest. Genuinely it is good and instructive information. Any Language Translation Service to English Thankful to you for sharing an article like this.
Post a Comment