File Coverage

blib/lib/Locale/VersionedMessages.pm
Criterion Covered Total %
statement 273 319 85.5
branch 108 150 72.0
condition 14 24 58.3
subroutine 26 27 96.3
pod 12 12 100.0
total 433 532 81.3


line stmt bran cond sub pod time code
1             package Locale::VersionedMessages;
2             # Copyright (c) 2010-2014 Sullivan Beck. All rights reserved.
3             # This program is free software; you can redistribute it and/or modify it
4             # under the same terms as Perl itself.
5              
6             ########################################################################
7              
8             require 5.008;
9 4     4   92236 use strict;
  4         9  
  4         174  
10 4     4   23 use warnings;
  4         8  
  4         641  
11              
12             our $VERSION;
13             $VERSION='0.95';
14              
15             ########################################################################
16             # METHODS
17             ########################################################################
18              
19             sub new {
20 4     4 1 51 my($class) = @_;
21              
22 4         30 my $self = {
23             'err' => '',
24             'set' => {},
25             'mess' => {},
26             'search' => [],
27             };
28              
29 4         12 bless $self, $class;
30              
31 4         13 return $self;
32             }
33              
34             sub version {
35 0     0 1 0 my($self) = @_;
36 0         0 return $VERSION;
37             }
38              
39             sub err {
40 46     46 1 205 my($self) = @_;
41 46         101 return $$self{'err'};
42             }
43              
44 4     4   22 no strict 'refs';
  4         23  
  4         1168  
45             sub set {
46 6     6 1 3727 my($self,@set) = @_;
47              
48 6         27 $$self{'err'} = '';
49 6 50       21 if (! @set) {
50 0         0 return sort keys %{ $$self{'set'} };
  0         0  
51             }
52              
53 6         15 foreach my $set (@set) {
54 6 50       44 if ($set !~ /^[a-zA-Z0-9_]+$/) {
55 0         0 $$self{'err'} = "Set must be alphanumeric/underscore: $set";
56 0         0 return;
57             }
58              
59 6         15 my $m = "Locale::VersionedMessages::Sets::$set";
60 6         468 eval "require $m";
61 6 100       1008 if ($@) {
62 1         3 chomp($@);
63 1         5 $$self{'err'} = "Unable to load set: $set: $@";
64 1         3 return;
65             }
66              
67 5         14 my $def_locale = ${ "${m}::DefaultLocale" };
  5         32  
68 5         11 my @all_locale = @{ "${m}::AllLocale" };
  5         31  
69 5         12 my %messages = %{ "${m}::Messages" };
  5         36  
70              
71 12         250 $$self{'set'}{$set} = { 'def_loc' => $def_locale,
72 5         35 'all_loc' => { map {$_,1} @all_locale },
73             'messages' => \%messages,
74             'search' => [],
75             };
76             }
77              
78 5         42 return;
79             }
80 4     4   22 use strict 'refs';
  4         11  
  4         1849  
81              
82             sub query_set_default {
83 1     1 1 68 my($self,$set) = @_;
84 1         3 $$self{'err'} = '';
85              
86 1 50       4 if (! exists $$self{'set'}{$set}) {
87 0         0 $$self{'err'} = "Set not loaded: $set";
88 0         0 return;
89             }
90              
91 1         5 return $$self{'set'}{$set}{'def_loc'};
92             }
93              
94             sub query_set_locales {
95 1     1 1 61 my($self,$set) = @_;
96 1         465 $$self{'err'} = '';
97              
98 1 50       5 if (! exists $$self{'set'}{$set}) {
99 0         0 $$self{'err'} = "Set not loaded: $set";
100 0         0 return;
101             }
102              
103 1         2 return sort keys %{ $$self{'set'}{$set}{'all_loc'} };
  1         12  
104             }
105              
106             sub query_set_msgid {
107 1     1 1 86 my($self,$set) = @_;
108 1         2 $$self{'err'} = '';
109              
110 1 50       4 if (! exists $$self{'set'}{$set}) {
111 0         0 $$self{'err'} = "Set not loaded: $set";
112 0         0 return;
113             }
114              
115 1         1 return sort keys %{ $$self{'set'}{$set}{'messages'} };
  1         6  
116             }
117              
118             sub search {
119 6     6 1 601 my($self,@locale) = @_;
120 6         14 $$self{'err'} = '';
121              
122 6         7 my $set;
123 6 100 100     37 if (@locale && exists $$self{'set'}{$locale[0]}) {
124 2         5 $set = shift(@locale);
125             }
126              
127 6 100 100     36 if ($set && @locale) {
    100          
    100          
128 1         7 $$self{'set'}{$set}{'search'} = [@locale];
129              
130             } elsif (@locale) {
131 3         9 $$self{'search'} = [@locale];
132              
133             } elsif ($set) {
134 1         5 $$self{'set'}{$set}{'search'} = [];
135              
136             } else {
137 1         4 $$self{'search'} = [];
138             }
139              
140 6         501 return;
141             }
142              
143             sub query_search {
144 6     6 1 6067 my($self,$set) = @_;
145 6         15 $$self{'err'} = '';
146              
147 6 100       62 if ($set) {
148 3 50       9 if (! exists $$self{'set'}{$set}) {
149 0         0 $$self{'err'} = "Set not loaded: $set";
150 0         0 return;
151             }
152              
153 3         2 return @{ $$self{'set'}{$set}{'search'} };
  3         10  
154             }
155              
156 3         4 return @{ $$self{'search'} };
  3         11  
157             }
158              
159 4     4   24 no strict 'refs';
  4         6  
  4         1066  
160             sub _load_lexicon {
161 8     8   12 my($self,$set,$locale) = @_;
162 8 100       23 return if (exists $$self{'mess'}{$set}{$locale});
163              
164 5 50       32 if ($set !~ /^[a-zA-Z0-9_]+$/) {
165 0         0 $$self{'err'} = "Set must be alphanumeric/underscore: $set";
166 0         0 return;
167             }
168              
169 5         13 my $m = "Locale::VersionedMessages::Sets::${set}::${locale}";
170 5         371 eval "require $m";
171 5 50       853 if ($@) {
172 0         0 chomp($@);
173 0         0 $$self{'err'} = "Unable to load lexicon: $set [$locale]: $@";
174 0         0 return;
175             }
176              
177 5         9 $$self{'mess'}{$set}{$locale} = { %{ "${m}::Messages" } };
  5         50  
178              
179 5         10 foreach my $msgid (sort keys %{ $$self{'mess'}{$set}{$locale} }) {
  5         36  
180 24 50       74 if (! exists $$self{'set'}{$set}{'messages'}{$msgid}) {
181 0         0 $$self{'err'} = "Undefined message ID in lexicon: $set [$locale $msgid]";
182 0         0 return;
183             }
184             }
185             }
186 4     4   28 use strict 'refs';
  4         7  
  4         5524  
187              
188             sub message {
189 26     26 1 53609 my($self,$set,$msgid,@args) = @_;
190 26         54 $$self{'err'} = '';
191              
192             # Parse arguments
193              
194 26         58 my($locale,%vals);
195 26 100 66     702 if (@args && @args % 2) {
196 22         34 $locale = shift(@args);
197 22         69 %vals = @args;
198 22 50       87 if (! exists $$self{'set'}{$set}{'all_loc'}{$locale}) {
199 0         0 $$self{'err'} = "Set not defined in locale: $set [ $locale ]";
200 0         0 return '';
201             }
202              
203             } else {
204 4         5 %vals = @args;
205             }
206              
207             # Look up the message.
208              
209 26         33 my @locale;
210 26 100 33     55 if ($locale) {
  4 50 66     21  
    100          
211 22         46 @locale = ($locale);
212              
213             } elsif (exists $$self{'set'}{$set}{'search'} &&
214 4         10 @{ $$self{'set'}{$set}{'search'} }) {
215 0         0 @locale = @{ $$self{'set'}{$set}{'search'} };
  0         0  
216              
217             } elsif (exists $$self{'search'} &&
218             @{ $$self{'search'} }) {
219 3         2 @locale = (@{ $$self{'search'} }, $$self{'set'}{$set}{'def_loc'});
  3         7  
220              
221             } else {
222 1         4 @locale = ($$self{'set'}{$set}{'def_loc'});
223             }
224              
225 26         28 my $message;
226              
227 26         41 foreach my $l (@locale) {
228 27 50       75 next if (! exists $$self{'set'}{$set}{'all_loc'}{$l});
229              
230 27 100       71 if (! exists $$self{'mess'}{$set}{$l}) {
231 4         39 $self->_load_lexicon($set,$l);
232 4 50       550 if ($$self{'err'}) {
233 0 0       0 if (wantarray) {
234 0         0 return ('');
235             } else {
236 0         0 return '';
237             }
238             }
239             }
240              
241 27 100       86 if (exists $$self{'mess'}{$set}{$l}{$msgid}) {
242 24         29 $locale = $l;
243 24         61 $message = $$self{'mess'}{$set}{$l}{$msgid}{'text'};
244 24         34 last;
245             }
246             }
247              
248 26 100       116 if (! $message) {
249 2         8 $$self{'err'} = "Message not found in specified lexicons: $msgid";
250 2         10 return;
251             }
252              
253 24         80 $message = $self->_fix_message($set,$msgid,$message,$locale,%vals);
254              
255 24 100       57 if (wantarray) {
256 3         9 return ($message,$locale);
257             } else {
258 21         261 return $message;
259             }
260             }
261              
262             sub query_msg_locales {
263 3     3 1 231 my($self,$set,$msgid) = @_;
264 3         4 $$self{'err'} = '';
265              
266 3 50       9 if (! exists $$self{'set'}{$set}) {
267 0         0 $$self{'err'} = "Set not loaded: $set";
268 0         0 return ();
269             }
270 3 100       8 if (! exists $$self{'set'}{$set}{'messages'}{$msgid}) {
271 1         3 $$self{'err'} = "Message ID not defined in set: $set [$msgid]";
272 1         2 return ();
273             }
274              
275 2         2 my %all_loc = %{ $$self{'set'}{$set}{'all_loc'} };
  2         10  
276 2         3 my $def_loc = $$self{'set'}{$set}{'def_loc'};
277 2         3 delete $all_loc{$def_loc};
278              
279 2         3 my @locale = ($def_loc);
280 2         5 foreach my $locale (sort keys %all_loc) {
281 4         8 $self->_load_lexicon($set,$locale);
282 4 50       10 return () if ($$self{'err'});
283 4 100       11 if (exists $$self{'mess'}{$set}{$locale}{$msgid}) {
284 3         4 push(@locale,$locale);
285             }
286             }
287              
288 2         9 return @locale;
289             }
290              
291             sub query_msg_vers {
292 6     6 1 367 my($self,$set,$msgid,$locale) = @_;
293 6         8 $$self{'err'} = '';
294              
295 6 50       14 if (! exists $$self{'set'}{$set}) {
296 0         0 $$self{'err'} = "Set not loaded: $set";
297 0         0 return '';
298             }
299 6 50       11 if (! exists $$self{'set'}{$set}{'messages'}{$msgid}) {
300 0         0 $$self{'err'} = "Message ID not defined in set: $set [$msgid]";
301 0         0 return '';
302             }
303              
304 6 100       10 $locale = $$self{'set'}{$set}{'def_loc'} if (! $locale);
305              
306 6 50       13 if (! exists $$self{'set'}{$set}{'all_loc'}{$locale}) {
307 0         0 $$self{'err'} = "Lexicon not available for set: $set [$locale]";
308 0         0 return '';
309             }
310              
311 6 100       14 if (exists $$self{'mess'}{$set}{$locale}{$msgid}) {
312 5         12 return $$self{'mess'}{$set}{$locale}{$msgid}{'vers'};
313             }
314 1         2 return 0;
315             }
316              
317             ########################################################################
318             # MESSAGE SUBSTITUTIONS
319             ########################################################################
320              
321             # This takes a message and performs substitutions for each of
322             # the different substitution values.
323             #
324             sub _fix_message {
325 24     24   69 my($self,$set,$msgid,$message,$locale,%vals) = @_;
326              
327             # No substitutions.
328              
329 24         26 my @vals;
330 24 100       119 if (exists $$self{'set'}{$set}{'messages'}{$msgid}{'vals'}) {
331 21         20 @vals = @{ $$self{'set'}{$set}{'messages'}{$msgid}{'vals'} };
  21         85  
332             }
333 24 100       54 if (! @vals) {
334 4 100       12 if (%vals) {
335 1         6 $$self{'err'} = "Message does not contain substitutions, but " .
336             "values were supplied: $msgid";
337 1         5 return '';
338             }
339 3         4 return $message;
340             }
341              
342             # Check each substitution.
343              
344 20         43 foreach my $val (sort @vals) {
345 20         22 my $done;
346 20 100       44 if (! exists $vals{$val}) {
347 1         7 $$self{'err'} = "A required substitution value was not passed in: " .
348             "$msgid [$val]";
349 1         5 return '';
350             }
351 19         50 ($message,$done) = $self->_substitute($set,$msgid,$locale,
352             $message,$val,$vals{$val});
353 19 100       61 return '' if ($$self{'err'});
354 13 100       24 if (! $done) {
355 1         6 $$self{'err'} = "The message in a lexicon does not contain a required " .
356             "substitution: $msgid [$locale $val]";
357 1         5 return '';
358             }
359 12         51 delete $vals{$val};
360             }
361 12         37 foreach my $val (sort keys %vals) {
362 1         5 $$self{'err'} = "An invalid value was passed in: $msgid [$val]";
363 1         4 return '';
364             }
365              
366 11         35 return $message;
367             }
368              
369             # This does the acutal substitution for a single substitution value.
370             #
371             # If the substitution is found, $done = 1 will be returned.
372             #
373             sub _substitute {
374 19     19   37 my($self,$set,$msgid,$locale,$message,$var,$val) = @_;
375 19         25 my $done = 0;
376              
377             # Simple substitutions: [foo]
378              
379 19 100       247 if ($message =~ s/\[\s*$var\s*\]/$val/sg) {
380 4         8 $done = 1;
381             }
382              
383             # Formatted substitutions: [ foo : FORMAT ]
384              
385 19         114 my $fmt_re = qr/\s*:\s*(%.*?)/;
386              
387 19         205 while ($message =~ s/\[\s*$var$fmt_re\s*\]/__L_M_TMP__/s) {
388 3         10 my $fmt = $1;
389              
390 4     4   34 no warnings;
  4         9  
  4         238  
391 3         12 $val = sprintf($fmt,$val);
392 4     4   21 use warnings;
  4         7  
  4         1455  
393 3 100       9 if ($val eq $fmt) {
394 2         11 $$self{'err'} = "Invalid sprintf format: $msgid [$locale $var]";
395 2         10 return;
396             }
397 1         5 $message =~ s/__L_M_TMP__/$val/s;
398 1         8 $done = 1;
399             }
400              
401             # Quant substitutions: [ foo : quant [ : FORMAT ] ... ]
402              
403 17         51 my ($msg,$d) = $self->_quant($set,$msgid,$locale,$message,$var,$val);
404 17 100       59 return if ($$self{'err'});
405 13         15 $message = $msg;
406 13 100       26 $done = $d if ($d);
407              
408 13         47 return ($message,$done);
409             }
410              
411             # This tests a string for any quant substitutions and returns the
412             # string.
413             #
414             sub _quant {
415 17     17   36 my($self,$set,$msgid,$locale,$mess,$var,$val) = @_;
416              
417 17         18 my $val_orig = $val;
418 17         51 my $fmt_re = qr/\s*:\s*(%.*?)/;
419 17         39 my $brack_re = qr/\s+\[([^]]*?)\]/;
420 17         36 my $sq_re = qr/\s+'([^']*?)'/;
421 17         38 my $dq_re = qr/\s+"([^"]*?)"/;
422 17         40 my $ws_re = qr/\s+(\S*)/;
423 17         279 my $tok_re = qr/(?:$brack_re|$sq_re|$dq_re|$ws_re)/;
424 17         25 my $tmp = '__L_M_TMP__';
425 17         20 my $done = 0;
426              
427             SUBST:
428 17         416 while ($mess =~ s/\[\s*$var\s*:\s*quant\s*(?:$fmt_re)?($tok_re+)\s*\]/$tmp/s) {
429 11         31 my $fmt = $1;
430 11         24 my $tokens= $2;
431              
432 11 50       23 if ($fmt) {
433 4     4   34 no warnings;
  4         4  
  4         188  
434 0         0 $val = sprintf($fmt,$val);
435 4     4   52 use warnings;
  4         7  
  4         3645  
436 0 0       0 if ($val eq $fmt) {
437 0         0 $$self{'err'} = "Invalid sprintf format: $msgid [$locale $var]";
438 0         0 return;
439             }
440             }
441              
442 11         13 my @tok;
443 11         132 while ($tokens =~ s/^$tok_re//) {
444 38   66     317 push(@tok, $1 || $2 || $3 || $4);
445             }
446              
447 11 100       30 if (@tok % 2 == 0) {
448 1         6 $$self{'err'} = "Default string required in quant substitution: " .
449             "$msgid [$locale $var]";
450 1         8 return;
451             }
452              
453             # @tok is (TEST, STRING, TEST, STRING, ..., DEFAULT_STRING)
454              
455 10         24 while (@tok) {
456 15         22 my $ele = shift(@tok);
457              
458             # DEFAULT_STRING
459              
460 15 100       34 if (! @tok) {
461 3         27 $ele =~ s/_$var/$val/g;
462 3         22 $mess =~ s/$tmp/$ele/s;
463 3         6 $done = 1;
464 3         25 next SUBST;
465             }
466              
467             # TEST, STRING
468              
469 12         36 my $flag = $self->_test($set,$msgid,$locale,$var,$val_orig,$ele);
470 12 100       53 return if ($$self{'err'});
471              
472 9         16 $ele = shift(@tok);
473 9 100       26 if ($flag) {
474 4         21 $ele =~ s/_$var/$val/g;
475 4         26 $mess =~ s/$tmp/$ele/s;
476 4         6 $done = 1;
477 4         35 next SUBST;
478             }
479             }
480             }
481              
482 13         92 return($mess,$done);
483             }
484              
485             # This parses a condition string and returns 1 if the condition is true for
486             # this value.
487             #
488             sub _test {
489 12     12   30 my($self,$set,$msgid,$locale,$var,$n,$test) = @_;
490              
491             # $n must be an unsigned integer
492              
493 12 100       62 if ($n !~ /^\d+$/) {
494 1         7 $$self{'err'} = "Quantity test requires an unsigned integer: " .
495             "$msgid [$locale $var]";
496 1         4 return;
497             }
498              
499             # Currently, test can only have:
500             # whitespace
501             # _VAR
502             # ( ) && || < <= == != >= >
503             # DIGITS
504             # in them.
505              
506 11         16 my $tmp = $test;
507 11         122 $tmp =~ s/(?:\s|\d|_$var|[()&|<=!>])//g;
508 11 100       29 if ($tmp) {
509 1         7 $$self{'err'} = "Quantity test contains invalid characters: " .
510             "$msgid [$locale $var]";
511 1         4 return;
512             }
513              
514             # Parse the tests.
515              
516             # 1) _VAR => $n
517              
518 10         94 $test =~ s/\s*_$var\s*/ $n /g;
519              
520             # 2) DIGITS % DIGITS => DIGITS
521              
522 10         42 while ($test =~ s/\s*(\d+)\s*%\s*(\d+)\s*/__L_M_TMP__/) {
523 0         0 my $m = $1 % $2;
524 0         0 $test =~ s/__L_M_TMP__/ $m /;
525             }
526              
527             # 3) DIGITS OP DIGITS => 0 or 1
528              
529 10         59 while ($test =~ s/\s*(\d+)\s*(==|>=|<=|!=|>|<)\s*(\d+)\s*/__L_M_TMP__/) {
530 11         36 my($m,$op,$n) = ($1,$2,$3);
531 11         11 my $x;
532 11 100       38 if ($op eq '==') {
    50          
    50          
    100          
    50          
    0          
533 9 100       29 $x = ($m==$n ? 1 : 0);
534             } elsif ($op eq '!=') {
535 0 0       0 $x = ($m!=$n ? 1 : 0);
536             } elsif ($op eq '>=') {
537 0 0       0 $x = ($m>=$n ? 1 : 0);
538             } elsif ($op eq '<=') {
539 1 50       4 $x = ($m<=$n ? 1 : 0);
540             } elsif ($op eq '>') {
541 1 50       9 $x = ($m>$n ? 1 : 0);
542             } elsif ($op eq '<') {
543 0 0       0 $x = ($m<$n ? 1 : 0);
544             }
545 11         64 $test =~ s/__L_M_TMP__/$x/;
546             }
547              
548             # Repeat until done:
549             # 4) (DIGITS) => DIGITS
550             # 5) DIGITS BOP DIGITS => 0 or 1
551              
552 10         10 while (1) {
553 11         15 my $done = 1;
554 11 100       32 if ($test =~ s/\s*\(\s*(\d+)\s*\)\s*/$1/g) {
555 1         2 $done = 0;
556             }
557 11         35 while ($test =~ s/\s*(\d+)\s*(\|\||&&)\s*(\d+)\s*/__L_M_TMP__/) {
558 1         4 my($m,$op,$n) = ($1,$2,$3);
559 1         3 my $x;
560 1 50       44 if ($op eq '&&') {
    0          
561 1 50 33     14 $x = ($m && $n ? 1 : 0);
562             } elsif ($op eq '||') {
563 0 0 0     0 $x = ($m || $n ? 1 : 0);
564             }
565 1         7 $test =~ s/__L_M_TMP__/$x/;
566 1         5 $done = 0;
567             }
568 11 100       25 last if ($done);
569             }
570              
571             # Final check:
572             # 6) DIGITS => 0 or 1
573              
574 10 100       42 if ($test =~ /^\s*(\d+)\s*$/) {
575 9 100       42 return ($1 ? 1 : 0);
576             }
577              
578 1         7 $$self{'err'} = "Quantity test malformed: $msgid [$locale $var]";
579 1         4 return;
580             }
581              
582             1;
583             # Local Variables:
584             # mode: cperl
585             # indent-tabs-mode: nil
586             # cperl-indent-level: 3
587             # cperl-continued-statement-offset: 2
588             # cperl-continued-brace-offset: 0
589             # cperl-brace-offset: 0
590             # cperl-brace-imaginary-offset: 0
591             # cperl-label-offset: 0
592             # End: