File Coverage

blib/lib/Lingua/Translator/Microsoft.pm
Criterion Covered Total %
statement 86 88 97.7
branch 6 8 75.0
condition 7 7 100.0
subroutine 24 24 100.0
pod 4 4 100.0
total 127 131 96.9


line stmt bran cond sub pod time code
1             package Lingua::Translator::Microsoft;
2 1     1   16089 use 5.10.0;
  1         2  
3              
4 1     1   474 use Moose;
  1         297588  
  1         5  
5 1     1   5122 use MooseX::HasDefaults::RO;
  1         8239  
  1         3  
6 1     1   6846 use MooseX::StrictConstructor;
  1         10289  
  1         3  
7 1     1   5580 use namespace::autoclean;
  1         2  
  1         5  
8              
9 1     1   52 use Carp;
  1         2  
  1         60  
10 1     1   654 use JSON;
  1         8050  
  1         3  
11 1     1   646 use LWP::UserAgent;
  1         29345  
  1         32  
12 1     1   469 use Params::Validate qw(:all);
  1         2152  
  1         166  
13 1     1   4 use URI;
  1         2  
  1         17  
14 1     1   691 use XML::Simple;
  1         5986  
  1         7  
15 1     1   492 use version;
  1         1259  
  1         5  
16              
17             our $VERSION = qv('1.1.0');
18              
19             has api_key => (
20             isa => 'Str',
21             required => 1,
22             );
23              
24             has app_id => (
25             isa => 'Str',
26             required => 1,
27             );
28              
29             has auth_url => (
30             isa => 'Str',
31             default => 'https://datamarket.accesscontrol.windows.net/v2/OAuth2-13',
32             );
33              
34             has api_url => (
35             isa => 'Str',
36             default => 'http://api.microsofttranslator.com/v2/Http.svc',
37             );
38              
39             # so that the token doesn't expire after checking it but before the request
40             # is processed on the other side.
41             has token_expiry_shift => (
42             isa => 'Str',
43             required => 0,
44             default => 10,
45             );
46              
47             has clock => (
48             isa => 'CodeRef',
49             default => sub {sub {time}}
50             );
51              
52             has ua_string => (
53             isa => 'Str',
54             default => "Lingua-Translator-Microsoft/$VERSION",
55             );
56              
57             has _token => (
58             is => 'rw',
59             isa => 'Maybe[Str]',
60             default => undef,
61             init_arg => undef,
62             );
63              
64             has _token_expiry => (
65             is => 'rw',
66             isa => 'Int',
67             default => 0,
68             init_arg => undef,
69             );
70              
71             has _ua_token => (
72             isa => 'LWP::UserAgent',
73             builder => '_build_ua_token',
74             lazy => 1,
75             init_arg => undef,
76             );
77              
78             has _ua_bing => (
79             isa => 'LWP::UserAgent',
80             builder => '_build_ua_bing',
81             lazy => 1,
82             init_arg => undef,
83             );
84              
85             sub _build_ua_token {
86 8     8   8 my $self = shift;
87 8         207 my $ua = LWP::UserAgent->new(
88             agent => $self->ua_string,
89             );
90 8         863 $ua->default_header(
91             'Content-Type' => 'application/x-www-form-urlencoded'
92             );
93 8         470 return $ua;
94             }
95              
96             sub _build_ua_bing {
97 8     8   7 my $self = shift;
98 8         214 return LWP::UserAgent->new(
99             agent => $self->ua_string,
100             #default_headers => {
101             # "Content-Type" => "text/xml",
102             #},
103             );
104             }
105              
106             sub translate {
107 11     11 1 2794 my $self = shift;
108 11         537 my ($from, $to, $text) = validate_pos(
109             @_,
110             {type => SCALAR, regex => '.+'},
111             {type => SCALAR, regex => '.+'},
112             {type => SCALAR},
113             );
114              
115             return $self->_make_api_call(
116             {
117             call => 'Translate',
118             method => 'GET',
119             args => {
120             text => $text,
121             from => $from,
122             to => $to,
123             },
124             process_response => sub {
125 4     4   257 my $r = shift;
126 4         9 my $xml = XML::Simple::XMLin($r);
127 4         4650 return $xml->{content};
128             }
129             }
130 5         100 );
131             }
132              
133             sub get_translations {
134 13     13 1 3663 my $self = shift;
135 13         449 my ($from, $to, $text, $opts) = validate_pos(
136             @_,
137             {type => SCALAR, regex => '.+'},
138             {type => SCALAR, regex => '.+'},
139             {type => SCALAR},
140             {type => HASHREF, regex => '.+', optional => 1},
141             );
142 6   100     125 $opts //= {};
143 6   100     21 my $max_translations = $opts->{max_translations} // 5;
144              
145             return $self->_make_api_call(
146             {
147             call => 'GetTranslations',
148             method => 'POST',
149             args => {
150             text => $text,
151             from => $from,
152             to => $to,
153             maxTranslations => $max_translations,
154             },
155             process_response => sub {
156 4     4   244 my $r = shift;
157 4         14 my $xml = XML::Simple::XMLin($r, ForceArray => 'TranslationMatch');
158 4         22442 my @translations = map { $_->{TranslatedText}->[0] } @{$xml->{Translations}->[0]->{TranslationMatch}};
  8         17  
  4         12  
159 4 50       50 return wantarray ? @translations : \@translations;
160             },
161             }
162 6         45 );
163             }
164              
165             sub detect {
166 7     7 1 981 my $self = shift;
167 7         342 my ($text) = validate_pos(@_, {type => SCALAR});
168              
169             return $self->_make_api_call(
170             {
171             call => 'Detect',
172             method => 'GET',
173             args => {
174             text => $text,
175             },
176             process_response => sub {
177 4     4   266 my $r = shift;
178 4         16 my $xml = XML::Simple::XMLin($r);
179 4         62689 return $xml->{content};
180             },
181             }
182 5         40 );
183             }
184              
185             sub speak {
186 9     9 1 1917 my $self = shift;
187 9         375 my ($language, $text, $opts) = validate_pos(
188             @_,
189             {type => SCALAR, regex => '.+'},
190             {type => SCALAR},
191             {type => HASHREF, regex => '.+', optional => 1},
192             );
193              
194             return $self->_make_api_call(
195             {
196             call => 'speak',
197             method => 'GET',
198             args => {
199             language => $language,
200             text => $text,
201             $opts ? %$opts : (),
202             },
203             process_response => sub {
204 4     4   225 my $r = shift;
205 4         22 return $r;
206             },
207             }
208 5 100       110 );
209             }
210              
211             sub _get_token {
212 16     16   16 my $self = shift;
213 16 100 100     445 return $self->_token if($self->_token && $self->clock->() < $self->_token_expiry);
214 12         346 my $ua = $self->_ua_token;
215 12         345 my $r = $ua->post($self->auth_url, {
216             grant_type => 'client_credentials',
217             client_id => $self->app_id,
218             client_secret => $self->api_key,
219             scope => "http://api.microsofttranslator.com",
220             });
221 12         11349 my $resp_data = JSON::decode_json $r->decoded_content;
222 12         1224 $self->_token($resp_data->{access_token});
223 12         315 $self->_token_expiry($self->clock->() + $resp_data->{expires_in} - $self->token_expiry_shift);
224 12         303 return $self->_token;
225             }
226              
227             sub _make_api_call {
228 16     16   16 my ($self, $args) = @_;
229              
230 16         28 my $method = lc $args->{method};
231 16         490 $self->_ua_bing->default_header(
232             'Authorization' => 'Bearer ' . $self->_get_token,
233             );
234 16         967 my $uri = URI->new($self->api_url . "/" . $args->{call});
235 16         5687 $uri->query_form(%{$args->{args}});
  16         73  
236 16         1368 my $response = $self->_ua_bing->$method( $uri->as_string );
237 16 50       20996 if($response->is_success) {
238 16         119 return $args->{process_response}->($response->decoded_content);
239             } else {
240 0           my @err = ($response->code, $response->message, $response->decoded_content);
241 0           croak "@err";
242             }
243             }
244              
245             __PACKAGE__->meta->make_immutable;
246              
247             1;
248              
249             =pod
250              
251             =encoding utf8
252              
253             =head1 NAME
254              
255             Lingua::Translator::Microsoft - A client library for the Microsoft Translator API
256              
257             =head1 SYNOPSIS
258              
259             my $api_key = read_file('/home/myapp/priv/translator.priv');
260             my $translator = Lingua::Translator::Microsoft->new(
261             api_key => $api_key,
262             app_id => $app_id,
263             );
264              
265             say $translator->translate('nl', 'en', 'voorbeeld'); # outputs 'example'
266              
267             my $wav = $translator->speak('de', 'Worüber man nicht sprechen kann, darüber muss man schweigen');
268             open(my $fh, ">", "tractatus.wav", {format => "mp3"});
269             print $fh $wav;
270             system("mplayer tractatus.wav");
271              
272             say $translator->detect("Ci vuole un fiore."); # outputs 'it'
273              
274             =head1 DESCRIPTION
275              
276             This is a client library for Microsoft's translate service. Currently you can use the following calls from the API:
277              
278             =over 4
279              
280             =item Translate
281              
282             =item GetTranslations
283              
284             =item Detect
285              
286             =item Speak
287              
288             =back
289              
290             All API-calling methods croak() unless they get a successful reply from the service.
291              
292             =head1 FUNCTIONS
293              
294             =head2 Lingua::Translator::Microsoft->new(api_key => $api_key, app_id => $app_id);
295              
296             Instantiate a new Lingua::Translator::Microsoft object.
297              
298             Arguments:
299              
300             =over 4
301              
302             =item
303              
304             api_key [required]
305              
306             The API key (client secret).
307              
308             =item
309              
310             app_id [required]
311              
312             Your application ID (client id). You need to register your application to be able to use the service.
313              
314             =item
315              
316             auth_url [optional]
317              
318             The URL to get the OAuth token from. Defaults to https://datamarket.accesscontrol.windows.net/v2/OAuth2-13. You probably don't need to change this.
319              
320             =item
321              
322             api_url [optional]
323              
324             The URL for the Microsoft Translator API (v2). Defaults to http://api.microsofttranslator.com/v2/Http.svc. You probably don't need to change this.
325              
326             =back
327              
328             Returns:
329              
330             =over 4
331              
332             =item
333              
334             A new Lingua::Translator::Microsoft instance.
335              
336             =back
337              
338             =head2 $translator->translate($source_language_code, $target_language_code, $text)
339              
340             Translate some text
341              
342             Arguments:
343              
344             =over 4
345              
346             =item
347              
348             source_language_code [required] (String)
349              
350             =item
351              
352             target_language_code [required] (String)
353              
354             =item
355              
356             text [required] (String)
357              
358             The text to translate.
359              
360             =back
361              
362             Returns:
363              
364             =over 4
365              
366             =item
367              
368             The translated text as a string.
369              
370             =back
371              
372             =head2 $translator->get_translations($source_language_code, $target_language_code, $text, { max_translations => 3})
373              
374             Translate some text (with multiple results).
375              
376             This function is sensitive to context. It returns an arrayref of translation in scalar context but a list of translations in list context.
377              
378             Arguments:
379              
380             =over 4
381              
382             =item
383              
384             source_language_code [required] (String)
385              
386             =item
387              
388             target_language_code [required] (String)
389              
390             =item
391              
392             text [required] (String)
393              
394             The text to translate.
395              
396             =item
397              
398             options [optional] (Hashref)
399              
400             A struct containing options to the call. For now the only option that you can put here is max_translations
401             which limits the number of results to a given number. max_translations defaults to 5.
402              
403             =back
404              
405             Returns:
406              
407             =over 4
408              
409             =item
410              
411             In list context the results as a list of strings (translations).
412              
413             =item
414              
415             In scalar context an arrayref of strings (translations).
416              
417             =back
418              
419             =head2 $translator->speak($language_code, $text)
420              
421             Pronounce some text
422              
423             Arguments:
424              
425             =over 4
426              
427             =item
428              
429             language_code [required] (String)
430              
431             =item
432              
433             text [required] (String)
434              
435             The text to synthetize.
436              
437             =back
438              
439             Returns:
440              
441             =over 4
442              
443             =item
444              
445             A wav stream containing the text spoken in the chosen language.
446              
447             =back
448              
449             =head2 $translator->detect($text)
450              
451             Detect the language of a text.
452              
453             Arguments:
454              
455             =over 4
456              
457             =item
458              
459             text [required] (String)
460              
461             The text to do language detection on.
462              
463             =back
464              
465             Returns:
466              
467             =over 4
468              
469             =item
470              
471             The code of the detected language.
472              
473             =back
474              
475             =head1 AUTHOR
476              
477             This module is written by Larion Garaczi <larion@cpan.org> (2016)
478              
479             =head1 SOURCE CODE
480              
481             The source code for this module is hosted on GitHub L<https://github.com/larion/lingua-translator-microsoft>.
482              
483             Feel free to contribute :)
484              
485             =head1 LICENSE AND COPYRIGHT
486              
487             This module is free software and is published under the same
488             terms as Perl itself.
489              
490             =cut