File Coverage

blib/lib/MetaTrans/Base.pm
Criterion Covered Total %
statement 48 360 13.3
branch 0 128 0.0
condition 0 24 0.0
subroutine 16 46 34.7
pod 26 26 100.0
total 90 584 15.4


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             MetaTrans::Base - Abstract base class for creating meta-translator plug-ins
4              
5             =head1 SYNOPSIS
6              
7             # This is not a working example. It serves for illustration only.
8             # For a working one see MetaTrans::UltralinguaNet source code.
9              
10             package MetaTrans::MyPlugin;
11              
12             use MetaTrans::Base;
13             use vars qw(@ISA);
14             @ISA = qw(MetaTrans::Base);
15              
16             use HTTP::Request;
17             use URI::Escape;
18              
19             sub new
20             {
21             my $class = shift;
22             my %options = @_;
23              
24             $options{host_server} = "www.some-online-translator.com"
25             unless (defined $options{host_server});
26              
27             my $self = new MetaTrans::Base(%options);
28             $self = bless $self, $class;
29              
30             # supported translation directions:
31             # English <-> German
32             # English <-> French
33             # English <-> Spanish
34              
35             $self->set_languages('eng', 'ger', 'fre', 'spa');
36              
37             $self->set_dir_1_to_all('eng');
38             $self->set_dir_all_to_1('eng');
39              
40             return $self;
41             }
42              
43             sub create_request
44             {
45             my $self = shift;
46             my $expression = shift;
47             my $src_lang_code = shift;
48             my $dest_lang_code = shift;
49              
50             # our-language-codes-to-server-language-codes conversion table
51             my %table = (eng => 'eng', ger => 'deu', fre => 'fra', spa => 'esp');
52              
53             return new HTTP::Request('GET',
54             'http://www.some-online-translator.com/translate.cgi?' .
55             'expr=' . uri_escape($expression) . '&' .
56             'src=' . $table{$src_lang_code} . '&' .
57             'dst=' . $table{$dest_lang_code}
58             );
59             }
60              
61             sub process_response
62             {
63             my $self = shift;
64             my $contents = shift;
65              
66             # we don't care about these here, but
67             # in some cases we might need to care
68             my $src_lang_code = shift;
69             my $dest_lang_code = shift;
70              
71             my @result;
72             while ($contents =~ m|
73             ([^<]*)
74             ([^<]*)
75             |gsix)
76             {
77             my $expression = $1;
78             my $translation = $2;
79              
80             # add some $expression and $translation normalization code here
81              
82             push @result, ($expression, $translation);
83             }
84            
85             return @result;
86             }
87              
88             1;
89              
90             =head1 DESCRIPTION
91              
92             This class serves as a base for creating C plug-ins,
93             especially those ones, which extract data from online translators.
94             Please see L first. C already contains
95             many features a C plug-in must have and makes creating
96             new plug-ins really easy.
97              
98             To perform a translation using an online translator (e.g.
99             L) one needs to do two things:
100              
101             =over 4
102              
103             =item 1. Emulate sending a form.
104              
105             =item 2. Process the HTML output webserver sends in response.
106              
107             =back
108              
109             To create a C plug-in using C one
110             only needs to do a bit more. The first step is to derrive
111             from C and "override" following two abstract
112             methods:
113              
114             =over 4
115              
116             =item $plugin->create_request($expression, $src_lang_code, $dest_lang_code)
117              
118             Should return a C object to be used by C
119             for retrieving HTML output, which contains translation of $expression from
120             the language with $src_lang_code to the language with $dest_lang_code.
121             This basicaly emulates sending a form.
122              
123             =item $plugin->process_response($contents, $src_lang_code, $dest_lang_code)
124              
125             This method should extract translations from the HTML code ($contents)
126             returned by webserver in response to the request. The translations must
127             be returned in an array of following form:
128              
129             (expression_1, translation_1, expression_2, translation_2, ...)
130              
131             B
132             In addition all expressions and their translations should be normalized
133             in a way so that all the grammar and meaning information were in parenthesis
134             or behind a semi-colon. For example, if you request a English to French
135             translation of "dog" from the L translator,
136             the first line of the result is
137              
138             dog n. : 1. chien n.m.,f. chienne 2. pitou n.m. (Familier) (Québécisme)
139              
140             The C module returns it as
141              
142             ('dog (n.)', 'chien (n.m.,f.)', 'dog (n.)', 'pitou (n.m.)')
143              
144             =back
145              
146             The next step is specifying list of languages supported by the plug-in.
147             We have to say, which languages we are able to translate from and which to.
148             This can be done easily by calling appropriate methods inherrited from
149             C. Please see L.
150              
151             The last step is setting the C attribute to the name of the
152             online translator used by the plug-in. See L.
153              
154             The C source code should serve as a good example
155             on how to create a C plug-in derrived from C.
156              
157             =cut
158              
159             package MetaTrans::Base;
160              
161 1     1   5 use strict;
  1         1  
  1         36  
162 1     1   5 use warnings;
  1         2  
  1         29  
163 1     1   5 use vars qw($VERSION @ISA @EXPORT_OK %EXPORT_TAGS %ENV);
  1         2  
  1         87  
164 1     1   5 use Exporter;
  1         2  
  1         36  
165 1     1   653 use MetaTrans::Languages qw(get_lang_by_code is_known_lang);
  1         2  
  1         87  
166              
167 1     1   8 use Carp;
  1         1404  
  1         74  
168 1     1   1017 use Encode;
  1         12998  
  1         92  
169 1     1   1250 use Getopt::Long;
  1         16146  
  1         7  
170 1     1   1165 use HTML::Entities;
  1         10115  
  1         132  
171 1     1   3755 use LWP::UserAgent;
  1         93136  
  1         47  
172 1     1   14 use HTTP::Response;
  1         2  
  1         132  
173              
174             $VERSION = do { my @r = (q$Revision: 1.3 $ =~ /\d+/g); sprintf "%d."."%02d", @r };
175             @ISA = qw(Exporter);
176             @EXPORT_OK = qw(is_exact_match is_match_at_start is_match_expr is_match_words
177             convert_to_utf8 M_EXACT M_START M_EXPR M_WORDS M_ALL);
178             %EXPORT_TAGS = (
179             match_consts => [qw(M_EXACT M_START M_EXPR M_WORDS M_ALL)],
180             match_funcs => [qw(is_exact_match is_match_at_start is_match_expr
181             is_match_words)],
182             );
183              
184              
185             # Expression matching types
186 1     1   7 use constant M_EXACT => 1; # exact match
  1         2  
  1         90  
187 1     1   8 use constant M_START => 2; # match at start
  1         2  
  1         58  
188 1     1   6 use constant M_EXPR => 3; # match expression
  1         3  
  1         50  
189 1     1   6 use constant M_WORDS => 4; # match words
  1         1  
  1         54  
190 1     1   5 use constant M_ALL => 5; # match anything to anything
  1         3  
  1         18099  
191              
192             =head1 CONSTRUCTOR METHODS
193              
194             =over 4
195              
196             =item MetaTrans::Base->new(%options)
197              
198             This method constructs a new MetaTrans::Base object and returns it. Key/value
199             pair arguments may be provided to set up the initial state. The following
200             options correspond to attribute methods described below:
201              
202             KEY DEFAULT
203             --------------- ----------------
204             host_server 'unknown.server'
205             script_name undef
206             timeout 5
207             matching M_START
208             match_at_bounds 1
209              
210             Please note that as long as the C is an abstract class,
211             calling the constructor method only makes sense in the derrived classes.
212              
213             =cut
214              
215             sub new
216             {
217 0     0 1   my $class = shift;
218 0           my %options = @_;
219              
220 0           my $self = bless {}, $class;
221              
222 0           my %defaults = (
223             host_server => 'unknown.server',
224             script_name => undef,
225             timeout => 5,
226             matching => M_START,
227             match_at_bounds => 1,
228             );
229              
230 0           foreach my $attr (keys %defaults)
231             {
232 0   0       $self->{$attr} = $options{$attr} || $defaults{$attr};
233             }
234              
235 0           return $self;
236             }
237              
238             =back
239              
240             =cut
241              
242              
243             =head1 ATTRIBUTES
244              
245             =over 4
246              
247             =item $plugin->host_server
248              
249             =item $plugin->host_server($name)
250              
251             Get/set the name of the online translator used by the plug-in. Is is only
252             used to inform the user where the translation comes from and hence can
253             be set to any meaningful value. It is a convention to set this to
254             the online translator base URL with the C<'http://'> stripped. For example,
255             the C sets C to
256             C<'www.ultralingua.net'>.
257              
258             =item $plugin->script_name
259              
260             =item $plugin->script_name($name)
261              
262             Get/set the name of the script, which runs this plug-in as a command line
263             application. The script uses this to identify itself when printing usage.
264             If unset, the script name is extracted from C<$0> variable. See the C
265             method.
266              
267             =item $plugin->timeout
268              
269             =item $plugin->timeout($secs)
270              
271             Get/set the time in seconds we want to wait for a reply from the online
272             translator before timing out.
273              
274             =item $plugin->matching
275              
276             =item $plugin->matching($type)
277              
278             Get/set the way of matching the found translations to the searched expression.
279             Some online translators in addition to the translation of the searched
280             expression also return translations of related expressions. For example,
281             we want to translate "dog" from English to French and we also get
282             translations of "dog days" or "every dog has his day". If this is not what
283             we want we can help ourselves by setting C to appropriate value:
284              
285             =over 8
286              
287             =item MetaTrans::Base::M_EXACT
288              
289             Match only those expressions which are the same as the searched one.
290             Matching is incasesensitive and ignores grammar information, i.e.
291             everything in parenthesis or after semi-colon. The same applies bellow.
292              
293             Examples:
294              
295             'Dog' matches 'dog' (incasesensitive)
296             'Hund' matches 'Hund; r' (grammar information ignored)
297             'dog' does not match 'dog bite' (not an exact match)
298              
299             =item MetaTrans::Base::M_START
300              
301             Match those expressions which are prefixed with the searched expression.
302              
303             Examples:
304              
305             'Dog' matches 'dog bite' (incasesensitive)
306             'Hund' matches 'Hund is los'
307             'Hund' does not match 'bissiger Hund' ('Hund' is not a prefix)
308              
309             =item MetaTrans::Base::M_EXPR
310              
311             Match those expressions which contain the searched expression, no matter
312             where.
313              
314             Examples:
315              
316             'Big Dog' matches 'very big dog'
317             'big dog' does not match 'big angry dog' ('big dog' is not a substring)
318              
319             =item MetaTrans::Base::M_WORDS
320              
321             Match those expressions which contain all the words of the searched
322             expression.
323              
324             Examples:
325              
326             'big dog' matches 'big angry dog'
327             'big dog' does not match 'angry dog' (not all words are contained)
328              
329             =item MetaTrans::Base::M_ALL
330              
331             Return all without any filtering.
332              
333             =back
334              
335             You can
336              
337             use MetaTrans::Base qw(:match_consts);
338              
339             to import matching constant names (C, C, ...) into your
340             program's namespace.
341              
342             =item $plugin->match_at_bounds
343              
344             =item $plugin->match_at_bounds($bool)
345              
346             Get/set the match-at-boundaries flag. Setting it to true value makes
347             matching behave in a slightly different way.
348             Subexpressions and words are matched at word boundaries only. In practice
349             this means that with C set to C the
350             expression "big dog"
351             won't be matched to "big angry doggie" while it would be with
352             match-at-boundaries set to false value. The same applies to
353             C and C. The option has no effect when C is set
354             to C or C.
355              
356             =item $plugin->default_dir
357              
358             =item $plugin->default_dir($src_lang_code, $dest_lang_code)
359              
360             Get/set the default translation direction. May only be set to supported one,
361             see L. Returns old value as an array of
362             two language codes.
363              
364             =back
365              
366             =cut
367              
368 0     0 1   sub host_server { shift->_elem('host_server', @_); }
369 0     0 1   sub script_name { shift->_elem('script_name', @_); }
370 0     0 1   sub timeout { shift->_elem('timeout', @_); }
371 0     0 1   sub match_at_bounds { shift->_elem('match_at_bounds', @_); }
372              
373             sub matching
374             {
375 0     0 1   my $self = shift;
376 0           my $type = shift;
377              
378 0           my %ok = (M_EXACT, 1, M_START, 1, M_EXPR, 1, M_WORDS, 1, M_ALL, 1);
379 0           my $old = $self->{matching};
380              
381 0 0         if (defined $type)
382             {
383 0 0         exists $ok{$type} ?
384             $self->{matching} = $type :
385             carp "invalid matching type: '$type'";
386             }
387              
388 0           return $old;
389             }
390              
391             sub default_dir
392             {
393 0     0 1   my $self = shift;
394 0           my $src_lang_code = shift;
395 0           my $dest_lang_code = shift;
396              
397 0           my @old_direction;
398 0 0 0       if (defined @{$self->{direction}} &&
  0            
  0            
399             $self->is_supported_dir(@{$self->{direction}}))
400             {
401 0           @old_direction = @{$self->{direction}};
  0            
402             }
403             else
404             {
405             # return `the first' supported translation direction
406 0           OUTER: foreach my $src_lang_code (@{$self->{language_keys}})
  0            
407             {
408 0           foreach my $dest_lang_code (@{$self->{language_keys}})
  0            
409             {
410 0 0         if ($self->is_supported_dir($src_lang_code, $dest_lang_code))
411             {
412 0           @old_direction = ($src_lang_code, $dest_lang_code);
413 0           last OUTER;
414             }
415             }
416             }
417             }
418              
419             return @old_direction
420 0 0 0       unless defined $src_lang_code && defined $dest_lang_code;
421              
422 0 0         if ($self->is_supported_dir($src_lang_code, $dest_lang_code))
423             {
424 0           carp "not supported direction: '${src_lang_code}2${dest_lang_code}'";
425 0           return @old_direction;
426             }
427              
428 0           @{$self->{direction}} = ($src_lang_code, $dest_lang_code);
  0            
429 0           return @old_direction;
430             }
431              
432             =head1 SPECIFYING SUPPORTED LANGUAGES
433              
434             Every C plug-in has to specify supported languages and translation
435             directions. C provides several methods for doing so. The
436             first step is specifying list of all languages, which appear on the left or
437             right side of any of supported translation directions. Consider your plug-in
438             supports following ones:
439              
440             English -> French
441             English -> German
442             French -> Spanish
443              
444             Then the list of supported languages is simply English, French, German and
445             Spanish.
446              
447             The arguments passed to particular methods need to be language codes, not
448             language names. Please see L for a complete list.
449              
450             =over 4
451              
452             =item $plugin->set_languages(@language_codes)
453              
454             Set supported languages to the ones specified by C<@language_codes>. In the
455             above exapmle one would call:
456              
457             $plugin->set_languages('eng', 'fre', 'ger', 'spa');
458              
459             =cut
460              
461             sub set_languages
462             {
463 0     0 1   my $self = shift;
464 0           my @language_codes = @_;
465              
466 0           foreach (@language_codes)
467             {
468 0 0         unless (is_known_lang($_))
469             {
470 0           carp "unknown language code: '$_', ignoring it";
471 0           next;
472             }
473              
474 0           ${$self->{languages}}{$_} = get_lang_by_code($_);
  0            
475 0           push @{$self->{language_keys}}, $_; # to keep ordering
  0            
476             }
477             }
478              
479             =item $plugin->set_dir_1_to_1($src_lang_code, $dest_lang_code)
480              
481             Add support for translating from language with C<$src_lang_code> to language
482             with C<$dest_lang_code>. Both languages need to be previously declared as
483             supported. The method returns true value on success, false value on error. To
484             specify we support directions from the above example we would simply call:
485              
486             $plugin->set_dir_1_to_1('eng', 'fre');
487             $plugin->set_dir_1_to_1('eng', 'ger');
488             $plugin->set_dir_1_to_1('fre', 'spa');
489              
490             =cut
491              
492             sub set_dir_1_to_1
493             {
494 0     0 1   my $self = shift;
495 0           my $src_lang_code = shift;
496 0           my $dest_lang_code = shift;
497              
498 0 0         unless (${$self->{languages}}{$src_lang_code})
  0            
499             {
500 0           carp "language '$src_lang_code' not supported, " .
501             "not setting '${src_lang_code}2${dest_lang_code}'";
502 0           return 0;
503             }
504              
505 0 0         unless (${$self->{languages}}{$dest_lang_code})
  0            
506             {
507 0           carp "language '$dest_lang_code' not supported, " .
508             "not setting '${src_lang_code}2${dest_lang_code}'";
509 0           return 0;
510             }
511              
512 0           ${$self->{directions}}{$src_lang_code . "2" . $dest_lang_code} = 1;
  0            
513 0           return 1;
514             }
515              
516             =item $plugin->unset_dir_1_to_1($src_lang_code, $dest_lang_code)
517              
518             Remove support for translating from language with C<$src_lang_code> to language
519             with C<$dest_lang_code>. Both languages need to be previously declared as
520             supported. The method returns true value on success, false value on error.
521              
522             =cut
523              
524             sub unset_dir_1_to_1
525             {
526 0     0 1   my $self = shift;
527 0           my $src_lang_code = shift;
528 0           my $dest_lang_code = shift;
529              
530 0 0         unless (${$self->{languages}}{$src_lang_code})
  0            
531             {
532 0           carp "language '$src_lang_code' not supported, " .
533             "not unsetting '${src_lang_code}2${dest_lang_code}'";
534 0           return 0;
535             }
536              
537 0 0         unless (${$self->{languages}}{$dest_lang_code})
  0            
538             {
539 0           carp "language '$dest_lang_code' not supported, " .
540             "not unsetting '${src_lang_code}2${dest_lang_code}'";
541 0           return 0;
542             }
543              
544 0           undef ${$self->{directions}}{$src_lang_code . "2" . $dest_lang_code};
  0            
545 0           return 1;
546             }
547              
548             =item $plugin->set_dir_1_to_spec($src_lang_code, @dest_lang_codes)
549              
550             Add support for translating from language with C<$src_lang_code> to all
551             languages whichs codes are in C<@dest_lang_codes>. The direction from
552             C<$src_lang_code> language to itself won't be set as supported even if
553             C<$src_lang_code> is specified in C<@dest_lang_codes>. However, calling
554              
555             $plugin->set_dir_1_to_1($src_lang_code, $src_lang_code);
556              
557             will do the job if this is what you want. It only results in warning messages
558             if some of the C<@dest_lang_codes> are unsupported. Only the supported ones
559             will be used, others are ignored. The method returns number of directions
560             set as supported on (partial) success, 0 on error.
561              
562             Example:
563              
564             my @all_languages = ('eng', 'fre', 'ger', 'spa');
565             $plugin->set_languages(@all_languages);
566             $plugin->set_dir_1_to_spec('eng', @all_languages);
567              
568             ... will result in following supported translation directions:
569              
570             English -> French
571             English -> German
572             English -> Spanish
573              
574             =cut
575              
576             sub set_dir_1_to_spec
577             {
578 0     0 1   my $self = shift;
579 0           my $src_lang_code = shift;
580 0           my @dest_lang_codes = @_;
581 0           my $set = 0;
582              
583 0 0         unless (${$self->{languages}}{$src_lang_code})
  0            
584             {
585 0           carp "language '$src_lang_code' not supported";
586 0           return $set;
587             }
588              
589 0           foreach my $dest_lang_code (@dest_lang_codes)
590             {
591 0 0         next if $dest_lang_code eq $src_lang_code;
592 0           $set += $self->set_dir_1_to_1($src_lang_code, $dest_lang_code);
593             }
594              
595 0           return $set;
596             }
597              
598             =item $plugin->set_dir_1_to_all($src_lang_code)
599              
600             This is just a shorter way for writting:
601              
602             $plugin->set_dir_1_to_spec($src_lang_code, @all_codes);
603              
604             where C<@all_codes> is an array of codes of all supported languages.
605              
606             =cut
607              
608             sub set_dir_1_to_all
609             {
610 0     0 1   my $self = shift;
611 0           my $src_lang_code = shift;
612              
613 0           return $self->set_dir_1_to_spec($src_lang_code, @{$self->{language_keys}});
  0            
614             }
615              
616              
617             =item $plugin->set_dir_spec_to_1($dest_lang_code, @src_lang_codes)
618              
619             This works exactly as C with reversed sides.
620              
621             =cut
622              
623             sub set_dir_spec_to_1
624             {
625 0     0 1   my $self = shift;
626 0           my $dest_lang_code = shift;
627 0           my @src_lang_codes = @_;
628 0           my $set = 0;
629              
630 0 0         unless (${$self->{languages}}{$dest_lang_code})
  0            
631             {
632 0           carp "language '$dest_lang_code' not supported";
633 0           return $set;
634             }
635              
636 0           foreach my $src_lang_code (@src_lang_codes)
637             {
638 0 0         next if $src_lang_code eq $dest_lang_code;
639 0           $set += $self->set_dir_1_to_1($src_lang_code, $dest_lang_code);
640             }
641              
642 0           return $set;
643             }
644              
645             =item $plugin->set_dir_all_to_1($dest_lang_code)
646              
647             This is just a shorter way for writting:
648              
649             $plugin->set_dir_spec_to_1($dest_lang_code, @all_codes);
650              
651             where C<@all_codes> is an array of codes of all supported languages.
652             Example:
653              
654             my @src_lang_codes = ('ger', 'fre', 'spa');
655             $plugin->set_languages('eng', 'por', @src_lang_codes);
656             $plugin->set_dir_spec_to_1('eng', @src_lang_codes);
657              
658             ... will result in following supported translation directions:
659              
660             German -> English
661             French -> English
662             Spanish -> English
663              
664             But if we replaced the last line with
665              
666             $plugin->set_dir_all_to_1('eng');
667              
668             the result would have been:
669              
670             Portuguese -> English
671             German -> English
672             French -> English
673             Spanish -> English
674              
675             =cut
676              
677             sub set_dir_all_to_1
678             {
679 0     0 1   my $self = shift;
680 0           my $dest_lang_code = shift;
681              
682 0           return $self->set_dir_spec_to_1($dest_lang_code,
683 0           @{$self->{language_keys}});
684             }
685              
686             =back
687              
688             =cut
689              
690             =head1 PLUG-IN REQUIRED METHODS
691              
692             These are the methods C expects every plug-in to provide. You only
693             need to worry about this if you are writting a plug-in from a scratch. If you
694             are derriving from C all these methods are inherited. They
695             make use of the abstract methods C and C,
696             attribute values and supported translation directions specified using
697             C methods. If you only want to use C as a base
698             class for your plug-in you can stop reading here. Everything you need to know
699             was written above.
700              
701             If you are writting a plug-in from a scratch you have to make sure it provides
702             all the methods with appropriate functionality specified in this section. In
703             addition, every C plug-in has to provide attribute methods
704             as specified in L section.
705              
706             =cut
707              
708             =over 4
709              
710             =item $plugin->is_supported_dir($src_lang_code, $dest_lang_code)
711              
712             Returns true value if the translation direction is supported from language with
713             C<$src_lang_code> to language with C<$dest_lang_code>, false value otherwise.
714              
715             =cut
716              
717             sub is_supported_dir
718             {
719 0     0 1   my $self = shift;
720 0           my $src_lang_code = shift;
721 0           my $dest_lang_code = shift;
722              
723 0           return ${$self->{directions}}{$src_lang_code . "2" . $dest_lang_code};
  0            
724             }
725              
726             =item $plugin->get_all_src_lang_codes
727              
728             Returns a list of all language codes, which the plug-in is able to translate
729             from. For example, C<('eng', 'fre')> will be returned if supported translation
730             directions are:
731              
732             English -> French
733             English -> Spanish
734             French -> Spanish
735              
736             =cut
737              
738             sub get_all_src_lang_codes
739             {
740 0     0 1   my $self = shift;
741 0           my @result;
742              
743 0           OUTER: foreach my $src_lang_code (@{$self->{language_keys}})
  0            
744             {
745 0           foreach my $dest_lang_code (@{$self->{language_keys}})
  0            
746             {
747 0 0         if ($self->is_supported_dir($src_lang_code, $dest_lang_code))
748             {
749 0           push @result, $src_lang_code;
750 0           next OUTER;
751             }
752             }
753             }
754              
755 0           return @result;
756             }
757              
758             =item $plugin->get_dest_lang_codes_for_src_lang_code($src_lang_code)
759              
760             Returns a list of all language codes, which the plug-in is able to translate
761             to from the language with $src_lang_code. If called with C<'eng'> as an
762             parameter in the above example, returned value would be C<('fre', 'spa')>.
763              
764             =cut
765              
766             sub get_dest_lang_codes_for_src_lang_code
767             {
768 0     0 1   my $self = shift;
769 0           my $src_lang_code = shift;
770 0           my @result;
771              
772 0           foreach my $dest_lang_code (@{$self->{language_keys}})
  0            
773             {
774 0 0         push @result, $dest_lang_code
775             if $self->is_supported_dir($src_lang_code, $dest_lang_code);
776             }
777              
778 0           return @result;
779             }
780              
781             =item $plugin->translate($expression [, $src_lang_code, $dest_lang_code])
782              
783             Returns translation of C<$expression> as an array of expression-translation
784             pairs in one string separated by C<" = "> in B.
785             An example output is:
786              
787             ("dog = chien", "dog = pitou", "dog days = canicule")
788              
789             C value is returned and an error printed if C<< $src_lang_code
790             -> $dest_lang_code >> is an unsupported translation direction. C<'timeout'>
791             string is returned if timeout occurs when querying online translator,
792             C<'error'> string is returned on any other error.
793              
794             Default translation direction (see C attribute) is used if
795             the method is called with first argument only.
796              
797             =cut
798              
799             sub translate
800             {
801 0     0 1   my $self = shift;
802 0           my $expression = shift;
803 0           my $src_lang_code = shift;
804 0           my $dest_lang_code = shift;
805              
806 0 0         unless (scalar(keys %{$self->{directions}}) > 0)
  0            
807             {
808 0           carp "no supported directions defined";
809 0           return 'error';
810             }
811              
812 0 0 0       ($src_lang_code, $dest_lang_code) = $self->default_dir
813             unless (defined $src_lang_code && defined $dest_lang_code);
814              
815 0 0         unless ($self->is_supported_dir($src_lang_code, $dest_lang_code))
816             {
817 0           carp "not supported direction: '${src_lang_code}2${dest_lang_code}'";
818 0           return 'error';
819             }
820              
821 0           my $ua = new LWP::UserAgent;
822 0           $ua->cookie_jar({ file => "$ENV{HOME}/.metatrans.cookies.txt" });
823 0           $ua->timeout($self->{timeout});
824              
825             # strip blanks
826 0           $expression =~ s/\s+/ /g;
827 0           $expression =~ s/^ //;
828 0           $expression =~ s/ $//;
829              
830 0           my $request = $self->create_request($expression, $src_lang_code,
831             $dest_lang_code);
832 0           my $response = $ua->request($request);
833              
834 0 0         if ($response->is_error())
835             {
836 0 0         if ($response->code =~ /50[03]/)
837             {
838 0           carp "timeout while translating '$expression'";
839 0           return 'timeout';
840             }
841             else
842             {
843 0           carp "error (" . $response->code .
844             ") while translating '$expression'";
845 0           return 'error';
846             }
847             }
848 0           my $content = $response->content();
849              
850 0           my @processed = $self->process_response($content, $src_lang_code,
851             $dest_lang_code);
852 0           my @result;
853              
854 0           my $at_bounds = $self->{match_at_bounds};
855 0           while (@processed > 0)
856             {
857 0           my $left = shift @processed;
858 0           my $right = shift @processed;
859              
860             next unless
861 0 0         $self->{matching} == M_EXACT ?
    0          
    0          
    0          
    0          
862             &is_exact_match($expression, $left) :
863             $self->{matching} == M_START ?
864             &is_match_at_start($expression, $left, $at_bounds) :
865             $self->{matching} == M_EXPR ?
866             &is_match_expr($expression, $left, $at_bounds) :
867             $self->{matching} == M_WORDS ?
868             &is_match_words($expression, $left, $at_bounds) :
869             1;
870              
871 0           push @result, "$left = $right";
872             }
873              
874 0           return @result;
875             }
876              
877             =item $plugin->get_trans_command($expression, $src_lang_code, $dest_lang_code,
878             $append)
879              
880             This method is a very ugly hack, for which writting C plug-ins from
881             a scratch is discouraged. See L for more information on why this
882             it is required.
883              
884             The C method is expected to return an array containing
885             command, which if run using C function
886             will print translations of C<$expression> from C<$src_lang_code> language to
887             C<$dest_lang_code> language (the first element of the array is the program
888             name, list of arguments follows). The command also needs to contain options
889             correspondent to current plug-in attribute values and ensure appropriate
890             behaviour. Each line of the output must correspond to one translation and
891             have following form:
892              
893             expression = translation
894              
895             In addition, the C<$append string>, if specified, should be appendet to each
896             line of the output.
897              
898             =cut
899              
900             sub get_trans_command
901             {
902 0     0 1   my $self = shift;
903 0           my $expression = shift;
904 0           my $src_lang_code = shift;
905 0           my $dest_lang_code = shift;
906 0           my $append = shift;
907              
908 0           my $class = ref($self);
909              
910             # $append =~ s/"/\\"/g;
911             # $expression =~ s/"/\\"/g;
912              
913              
914             # my $command = "runtrans";
915             # $command.= " $class";
916             # $command.= " -t " . $self->{timeout};
917             # $command.= " -m " . ($self->{matching} == M_EXACT ? 'exact' :
918             # $self->{matching} == M_START ? 'start' :
919             # $self->{matching} == M_EXPR ? 'expr' :
920             # $self->{matching} == M_WORDS ? 'words' :
921             # 'all' );
922             # $command.= " -b " if $self->{match_at_bounds};
923             # $command.= " -d " . $src_lang_code . "2" . $dest_lang_code;
924             # $command.= " -a \"$append\"";
925             # $command.= " \"$expression\"";
926              
927 0           my @command;
928 0           push @command, "runtrans", $class;
929 0           push @command, "-t", $self->{timeout};
930 0 0         push @command, "-m", ($self->{matching} == M_EXACT ? 'exact' :
    0          
    0          
    0          
931             $self->{matching} == M_START ? 'start' :
932             $self->{matching} == M_EXPR ? 'expr' :
933             $self->{matching} == M_WORDS ? 'words' :
934             'all' );
935 0 0         push @command, "-b" if $self->{match_at_bounds};
936 0           push @command, "-d", $src_lang_code . "2" . $dest_lang_code;
937 0           push @command, "-a", $append;
938 0           push @command, $expression;
939              
940 0           return @command;
941             }
942              
943             =back
944              
945             =cut
946              
947             =head1 STATIC FUNCTIONS
948              
949             =over 4
950              
951             =item is_exact_match($in_expr, $found_expr)
952              
953             Returns true value if the C<$found_expr> expression matches input expression
954             C<$in_expr> when using C matching options (see C attribute).
955              
956             =cut
957              
958             sub is_exact_match
959             {
960 0     0 1   my $in_expr = shift;
961 0           my $found_expr = shift;
962              
963 0           return lc(&strip_grammar_info($in_expr)) eq
964             lc(&strip_grammar_info($found_expr));
965             }
966              
967             =item is_match_at_start($in_expr, $found_expr, $at_bounds)
968              
969             Returns true value if the C<$found_expr> expression matches input expression
970             C<$in_expr> when using C matching options (see C attribute).
971             The C<$at_bounds> argument corresponds to the C attribute.
972              
973             =cut
974              
975             sub is_match_at_start
976             {
977 0     0 1   my $in_expr = shift;
978 0           my $found_expr = shift;
979 0           my $at_bounds = shift;
980              
981 0           my $in_stripped = &strip_grammar_info($in_expr);
982 0           my $found_stripped = &strip_grammar_info($found_expr);
983              
984 0 0         return $at_bounds ?
985             $found_stripped =~ /^\Q$in_stripped\E\b/g :
986             $found_stripped =~ /^\Q$in_stripped\E/g ;
987             }
988              
989             =item is_match_expr($in_expr, $found_expr, $at_bounds)
990              
991             Returns true value if the C<$found_expr> expression matches input expression
992             C<$in_expr> when using C matching options (see C attribute).
993             The C<$at_bounds> argument corresponds to the C attribute.
994              
995             =cut
996              
997             sub is_match_expr
998             {
999 0     0 1   my $in_expr = shift;
1000 0           my $found_expr = shift;
1001 0           my $at_bounds = shift;
1002              
1003 0           my $in_stripped = &strip_grammar_info($in_expr);
1004 0           my $found_stripped = &strip_grammar_info($found_expr);
1005              
1006 0 0         return $at_bounds ?
1007             $found_stripped =~ /\b\Q$in_stripped\E\b/g :
1008             $found_stripped =~ /\Q$in_stripped\E/g ;
1009             }
1010              
1011             =item is_match_words($in_expr, $found_expr, $at_bounds)
1012              
1013             Returns true value if the C<$found_expr> expression matches input expression
1014             C<$in_expr> when using C matching options (see C attribute).
1015             The C<$at_bounds> argument corresponds to the C attribute.
1016              
1017             =cut
1018              
1019             sub is_match_words
1020             {
1021 0     0 1   my $in_expr = shift;
1022 0           my $found_expr = shift;
1023 0           my $at_bounds = shift;
1024              
1025 0           my $in_stripped = &strip_grammar_info($in_expr);
1026 0           my $found_stripped = &strip_grammar_info($found_expr);
1027              
1028 0           foreach my $word (split /\W+/, $in_stripped)
1029             {
1030             return undef
1031 0 0         unless $at_bounds ?
    0          
1032             $found_stripped =~ /\b\Q$word\E\b/g :
1033             $found_stripped =~ /\Q$word\E/g ;
1034             }
1035              
1036 0           return 1;
1037             }
1038              
1039             =item strip_grammar_info($expression)
1040              
1041             Returns the C<$expression> with all the grammar and meaning information deleted
1042             (everything in parantheses or behind a semicolon) B
1043             format> (see L).
1044              
1045             =cut
1046              
1047             sub strip_grammar_info
1048             {
1049 0     0 1   my $expr = shift;
1050 0 0         $expr = Encode::decode_utf8($expr)
1051             unless Encode::is_utf8($expr);
1052 0           $expr =~ s/\([^)]*\)//g;
1053             #$expr =~ s/, (r|e|s)\s*$//;
1054 0           $expr =~ s/;.*//;
1055 0           $expr =~ s/\W+/ /g;
1056 0           $expr =~ s/^ //;
1057 0           $expr =~ s/ $//;
1058 0           return $expr;
1059             }
1060              
1061             =item convert_to_utf8($input_encoding, $string)
1062              
1063             Converts C<$string> from C<$input_encoding> to UTF-8 encoding. In addition all
1064             HTML entities contained in the C<$string> are converted to corresponding
1065             UTF-8 characters. This may sometimes be very useful when writting the
1066             C method.
1067              
1068             =cut
1069              
1070             sub convert_to_utf8
1071             {
1072 0     0 1   my $input_encoding = shift;
1073 0           my $string = shift;
1074              
1075 0           $string = Encode::decode($input_encoding, $string);
1076 0           my $str_unescaped = HTML::Entities::decode_entities($string);
1077              
1078             # $str_escaped might be in Perl's internal format, need to encode it
1079 0 0         return Encode::is_utf8($str_unescaped) ?
1080             Encode::encode_utf8($str_unescaped) :
1081             $str_unescaped;
1082             }
1083              
1084             =back
1085              
1086             =cut
1087              
1088             =head1 OTHER METHODS
1089              
1090             =over 4
1091              
1092             =item $plugin->run
1093              
1094             Run the plug-in as a command line application. Very useful for testing and
1095             debugging. Try executing following script to see what this does:
1096              
1097             #!perl
1098              
1099             # load a plug-in class derrived from MetaTrans::Base
1100             use MetaTrans::UltralinguaNet;
1101              
1102             # instantiate an object
1103             my $plugin = new MetaTrans::UltralinguaNet;
1104              
1105             # run it
1106             $plugin->run;
1107              
1108             =cut
1109              
1110             sub run
1111             {
1112 0     0 1   my $self = shift;
1113              
1114 0           croak "no supported directions defined"
1115 0 0         unless (scalar(keys %{$self->{directions}}) > 0);
1116              
1117 0           my @options = $self->_get_options();
1118             return
1119 0 0         if @options < 7;
1120              
1121 0           my ($timeout, $matching, $at_bounds, $src_lang_code, $dest_lang_code,
1122             $append, $help) = @options;
1123              
1124 0 0 0       if ($help || @ARGV == 0)
1125             {
1126 0           $self->_print_usage();
1127 0           return;
1128             }
1129              
1130 0           $self->timeout($timeout);
1131 0           $self->match_at_bounds($at_bounds);
1132 0           $self->matching($matching);
1133              
1134 0           my $state;
1135 0           my $i = 0;
1136 0           foreach my $expr (@ARGV)
1137             {
1138 0           $i++;
1139              
1140 0           my @translations = $self->translate($expr, $src_lang_code,
1141             $dest_lang_code);
1142              
1143 0 0 0       if (@translations && $translations[0] !~ /=/)
1144             {
1145 0           $state = $translations[0];
1146 0           next;
1147             }
1148 0           $state = "ok";
1149              
1150 0           foreach my $trans (@translations)
1151 0           { print "$trans$append\n"; }
1152              
1153 0 0         print "\n" unless $i == @ARGV;
1154             }
1155              
1156 0 0         print $state . $append . "\n"
1157             if $append;
1158             }
1159              
1160             =back
1161              
1162             =cut
1163              
1164             ################################################################################
1165             # private methods #
1166             ################################################################################
1167              
1168             sub _get_options
1169             {
1170 0     0     my $self = shift;
1171              
1172 0           my $timeout = $self->{timeout};
1173 0           my $matching_str;
1174 0           my $matching = $self->{timeout};
1175 0           my $at_bounds;
1176             my $direction;
1177 0           my $help;
1178 0           my $append = '';
1179              
1180 0           Getopt::Long::Configure("bundling");
1181 0           GetOptions(
1182             't=i' => \$timeout,
1183             'm=s' => \$matching_str,
1184             'b' => \$at_bounds,
1185             'd=s' => \$direction,
1186             'a=s' => \$append,
1187             'h' => \$help,
1188             );
1189              
1190 0 0         if (defined $matching_str)
1191             {
1192             $matching_str eq 'exact' ? $matching = M_EXACT :
1193             $matching_str eq 'start' ? $matching = M_START :
1194             $matching_str eq 'expr' ? $matching = M_EXPR :
1195             $matching_str eq 'words' ? $matching = M_WORDS :
1196             $matching_str eq 'all' ? $matching = M_ALL :
1197             do
1198 0 0         {
    0          
    0          
    0          
    0          
1199 0           warn "invalid matching type: '$matching_str'\n";
1200 0           return undef;
1201             }
1202             }
1203              
1204 0 0 0       if (defined $direction && $direction !~ /2/)
1205             {
1206 0           warn "invalid direction format: '$direction'\n";
1207 0           return undef;
1208             }
1209              
1210 0 0         my ($src_lang_code, $dest_lang_code) = defined $direction ?
1211             split /2/, $direction :
1212             undef;
1213              
1214 0           return ($timeout, $matching, $at_bounds, $src_lang_code, $dest_lang_code,
1215             $append, $help);
1216             }
1217              
1218             sub _print_usage
1219             {
1220 0     0     my $self = shift;
1221 0           my $host = $self->{host_server};
1222 0           my $script = $self->{script_name};
1223 0           my $timeout = $self->{timeout};
1224 0           my $matching = $self->{matching};
1225              
1226 0 0         unless (defined $script)
1227             {
1228 0           $script = $0;
1229 0           $script =~ s|^.*/||;
1230             }
1231              
1232 0           my ($def_exact, $def_start, $def_expr, $def_words, $def_all) =
1233             ('', '', '', '', '');
1234              
1235 0           my $def_str = '(def)';
1236 0 0         $matching == M_EXACT ? $def_exact = $def_str :
    0          
    0          
    0          
1237             $matching == M_START ? $def_start = $def_str :
1238             $matching == M_EXPR ? $def_expr = $def_str :
1239             $matching == M_WORDS ? $def_words = $def_str :
1240             $def_all = $def_str ;
1241              
1242 0           my ($def_src_lang_code, $def_dest_lang_code) = $self->default_dir();
1243 0           my ($wd, $wl, $wr) = $self->_get_column_widths();
1244              
1245 0           my @dir_options;
1246 0           foreach my $src_lang_code (@{$self->{language_keys}})
  0            
1247             {
1248 0           foreach my $dest_lang_code (@{$self->{language_keys}})
  0            
1249             {
1250 0 0         next unless $self->is_supported_dir($src_lang_code,
1251             $dest_lang_code);
1252              
1253 0           my $dir_option = sprintf("%-${wd}s: %-${wl}s -> %-${wr}s",
1254             $src_lang_code . "2" . $dest_lang_code,
1255 0           ${$self->{languages}}{$src_lang_code},
1256 0           ${$self->{languages}}{$dest_lang_code});
1257              
1258 0 0 0       $dir_option .= " (default)"
1259             if ($src_lang_code eq $def_src_lang_code &&
1260             $dest_lang_code eq $def_dest_lang_code);
1261              
1262 0           push @dir_options, $dir_option;
1263             }
1264             }
1265              
1266 0           my $indent = " ";
1267 0           my $directions = join("\n$indent", @dir_options);
1268              
1269 0           print <
1270             Multilingual dictionary metasearcher for $host
1271             Usage: $script [options] expression [...]\ttranslate word(s)
1272              
1273             Options:
1274             -- expressions to be translated follow
1275             -t wait for the response for secs (default $timeout)
1276             -m set matching type
1277             exact: exact match only $def_exact
1278             start: match at start of the translated expr. only $def_start
1279             expr : match expr. anywhere in the translated expr. $def_expr
1280             words: match expr. words in the translated expr. $def_words
1281             all : match anything to anything $def_all
1282             -b match at word boundaries only
1283             -d set translation direction
1284             $directions
1285             -a append to each line of output
1286             -h print this help screen
1287             EOF
1288             }
1289              
1290             sub _get_column_widths
1291             {
1292 0     0     my $self = shift;
1293              
1294 0           my $max_dir_width = 0;
1295 0           my $max_lcol_width = 0;
1296 0           my $max_rcol_width = 0;
1297            
1298 0           foreach my $src_lang_code (@{$self->{language_keys}})
  0            
1299             {
1300 0           foreach my $dest_lang_code (@{$self->{language_keys}})
  0            
1301             {
1302 0 0         next unless $self->is_supported_dir($src_lang_code,
1303             $dest_lang_code);
1304 0           my $dir_width = length($src_lang_code . "2" . $dest_lang_code);
1305 0           my $lcol_width = length(${$self->{languages}}{$src_lang_code});
  0            
1306 0           my $rcol_width = length(${$self->{languages}}{$dest_lang_code});
  0            
1307              
1308 0 0         $max_dir_width = $dir_width
1309             if $dir_width > $max_dir_width;
1310              
1311 0 0         $max_lcol_width = $lcol_width
1312             if $lcol_width > $max_lcol_width;
1313              
1314 0 0         $max_rcol_width = $rcol_width
1315             if $rcol_width > $max_rcol_width;
1316             }
1317             }
1318              
1319 0           return ($max_dir_width, $max_lcol_width, $max_rcol_width);
1320             }
1321              
1322             # borrowed from LWP::MemberMixin
1323             sub _elem
1324             {
1325 0     0     my($self, $elem, $val) = @_;
1326 0           my $old = $self->{$elem};
1327 0 0         $self->{$elem} = $val if defined $val;
1328 0           return $old;
1329             }
1330              
1331             1;
1332              
1333             __END__