File Coverage

blib/lib/Rose/HTML/Object/Message/Localizer.pm
Criterion Covered Total %
statement 362 412 87.8
branch 151 232 65.0
condition 86 150 57.3
subroutine 56 62 90.3
pod 21 40 52.5
total 676 896 75.4


line stmt bran cond sub pod time code
1             package Rose::HTML::Object::Message::Localizer;
2              
3 44     44   103460 use strict;
  44         113  
  44         1297  
4              
5 44     44   226 use Carp;
  44         119  
  44         2408  
6 44     44   19683 use Clone::PP();
  44         32912  
  44         909  
7 44     44   283 use Scalar::Util();
  44         100  
  44         674  
8              
9 44     44   9964 use Rose::HTML::Object::Errors();
  44         115  
  44         841  
10 44     44   263 use Rose::HTML::Object::Messages();
  44         192  
  44         898  
11              
12 44     44   244 use base 'Rose::Object';
  44         103  
  44         19940  
13              
14             our $VERSION = '0.606';
15              
16             our $Debug = 0;
17              
18 44     44   9999 use constant DEFAULT_VARIANT => 'default';
  44         122  
  44         3455  
19              
20             #
21             # Object data
22             #
23              
24             use Rose::Object::MakeMethods::Generic
25             (
26 44         574 'hash --get_set_init' =>
27             [
28             'localized_messages_hash',
29             ],
30              
31             'scalar --get_set_init' =>
32             [
33             'locale',
34             'message_class',
35             'messages_class',
36             'errors_class',
37             'error_class',
38             ],
39 44     44   22060 );
  44         257414  
40              
41             #
42             # Class data
43             #
44              
45             use Rose::Class::MakeMethods::Generic
46             (
47 44         335 inheritable_hash => 'default_locale_cascade',
48 44     44   24684 );
  44         198  
49              
50             use Rose::Class::MakeMethods::Generic
51             (
52 44         513 inheritable_scalar =>
53             [
54             'default_locale',
55             '_auto_load_messages',
56             '_auto_load_locales',
57             ],
58 44     44   9158 );
  44         120  
59              
60             __PACKAGE__->default_locale('en');
61             __PACKAGE__->default_locale_cascade('default' => [ 'en' ]);
62              
63             #
64             # Class methods
65             #
66              
67 0     0 1 0 sub default_variant { DEFAULT_VARIANT }
68              
69             #
70             # Object methods
71             #
72              
73 27     27 0 414 sub init_localized_messages_hash { {} }
74              
75             sub init_locale_cascade
76             {
77 26     26 0 102 my($self) = shift;
78 26   33     158 my $class = ref($self) || $self;
79 26         164 return $class->default_locale_cascade;
80             }
81              
82             sub locale_cascade
83             {
84 704     704 1 1286 my($self) = shift;
85              
86 704   66     1716 my $hash = $self->{'locale_cascade'} ||= ref($self)->init_locale_cascade;
87              
88 704 50       1872 if(@_)
89             {
90 704 50       1358 if(@_ == 1)
    0          
91             {
92 704         2520 return $hash->{$_[0]};
93             }
94             elsif(@_ % 2 == 0)
95             {
96 0         0 for(my $i = 0; $i < @_; $i += 2)
97             {
98 0         0 $hash->{$_[$i]} = $_[$i + 1];
99             }
100             }
101 0         0 else { croak "Odd number of arguments passed to locale_cascade()" }
102             }
103              
104 0 0       0 return wantarray ? %$hash : $hash;
105             }
106              
107             sub init_locale
108             {
109 33     33 0 1891 my($self) = shift;
110 33   33     164 my $class = ref($self) || $self;
111 33         310 return $class->default_locale;
112             }
113              
114 26     26 0 404 sub init_messages_class { 'Rose::HTML::Object::Messages' }
115 35     35 0 2462 sub init_message_class { 'Rose::HTML::Object::Message::Localized' }
116 9     9 0 100 sub init_errors_class { 'Rose::HTML::Object::Errors' }
117 28     28 0 465 sub init_error_class { 'Rose::HTML::Object::Error' }
118              
119 0     0 0 0 sub clone { Clone::PP::clone(shift) }
120              
121             sub parent
122             {
123 0     0 1 0 my($self) = shift;
124 0 0       0 return Scalar::Util::weaken($self->{'parent'} = shift) if(@_);
125 0         0 return $self->{'parent'};
126             }
127              
128             sub localize_message
129             {
130 2214     2214 1 8689 my($self, %args) = @_;
131              
132 2214         4189 my $message = $args{'message'};
133              
134 2214 50 33     12260 return $message unless($message->can('text') && $message->can('id'));
135 2214 100       6051 return $message->text if($message->is_custom);
136              
137 351         742 my $parent = $message;
138              
139 351 50       1111 if($parent->can('parent'))
140             {
141 351         818 $parent = $parent->parent;
142             }
143              
144 351 100 100     1747 if($parent && $parent->isa('Rose::HTML::Object::Error'))
145             {
146 132         430 $parent = $parent->parent;
147             }
148              
149 351 100 33     1026 my $calling_class = $parent ? ref($parent) : $args{'caller'} || (caller)[0];
150              
151 351         601 my $first_parent = $parent;
152              
153 351   33     897 my $args = $args{'args'} || $message->args;
154 351   33     943 my $locale = $args{'locale'} || $message->locale || $self->locale;
155              
156 351         767 my $id = $message->id;
157              
158 351   33     1270 my $variant = $args{'variant'} ||=
159             $self->select_variant_for_message(id => $id,
160             args => $args,
161             locale => $locale);
162              
163 351   50     904 my $locale_cascade = $self->locale_cascade($locale) ||
164             $self->locale_cascade('default') || [];
165              
166 351         880 foreach my $try_locale ($locale, @$locale_cascade)
167             {
168 376   50     1100 my $variant_cascade =
169             $self->variant_cascade(locale => $try_locale,
170             variant => $variant,
171             message => $message,
172             args => $args) || [];
173              
174 376         843 foreach my $try_variant ($variant, @$variant_cascade)
175             {
176 391         1129 my $text =
177             $self->get_localized_message_text(
178             id => $id,
179             locale => $try_locale,
180             variant => $try_variant,
181             from_class => $calling_class);
182              
183 391         730 $parent = $first_parent;
184              
185             # Look for messages in parents
186 391   100     1186 while(!defined $text && $parent)
187             {
188 44 50       264 $parent = $parent->can('parent_field') ? $parent->parent_field :
    50          
    100          
189             $parent->can('parent_form') ? $parent->parent_form :
190             $parent->can('parent') ? $parent->parent :
191             undef;
192              
193 44 100       241 if($parent)
194             {
195 4         23 $text =
196             $self->get_localized_message_text(
197             id => $id,
198             locale => $try_locale,
199             variant => $try_variant,
200             from_class => ref($parent));
201             }
202             }
203              
204 391 100       1259 return $self->process_placeholders($text, $args) if(defined $text);
205             }
206             }
207              
208 0         0 return undef;
209             }
210              
211             # All this to avoid making Scalar::Defer a prerequisite....sigh.
212             sub _evaluate
213             {
214 44     44   49592 no warnings 'uninitialized';
  44         132  
  44         8697  
215 132 100   132   375 return $_[0] unless(ref $_[0] eq 'CODE');
216 38         100 return $_[0]->();
217             }
218              
219             sub process_placeholders
220             {
221 351     351 0 838 my($self, $text, $args) = @_;
222              
223 351 50       1173 my %args = $args ? %$args : ();
224              
225             # Values will be modified in-place
226 351         868 foreach my $value (values %args)
227             {
228 196 100       513 if(my $ref = ref($value))
229             {
230 68 100       152 if($ref eq 'ARRAY')
231             {
232 28         59 $value = [ map { _evaluate($_) } @$value ];
  92         157  
233             }
234             else
235             {
236 40         93 $value = _evaluate($value);
237             }
238             }
239             }
240              
241 44     44   346 no warnings 'uninitialized';
  44         137  
  44         37135  
242              
243 351         728 for($text)
244             {
245             # Process [@123(...)] and [@foo(...)] placeholders
246 351 100       1086 s{ ( (?:\\.|[^\[]*)* ) \[ \@ (\d+ | [a-zA-Z]\w* ) (?: \( (.*) \) )? \] }
  15 50       96  
  15         101  
247             { $1 . join(defined $3 ? $3 : ', ', ref $args{$2} ? @{$args{$2}} : $args{$2}) }gex;
248              
249 351         1362 # Process [123] and [foo] placeholders
250             s{ ( (?:\\.|[^\[]*)* ) \[ (\d+ | [a-zA-Z]\w* ) \] }{$1$args{$2}}gx;
251              
252 351         851 # Unescape escaped opening square brackets
253             s/\\\[/[/g;
254             }
255 351         4172  
256             return $text;
257             }
258 395     395 1 1208  
259 1     1 1 14 sub get_message_name { shift->messages_class->get_message_name(@_) }
260             sub get_message_id { shift->messages_class->get_message_id(@_) }
261 0     0 1 0  
262 4     4 1 37 sub get_error_name { shift->errors_class->get_error_name(@_) }
263             sub get_error_id { shift->errors_class->get_error_id(@_) }
264              
265             sub message_for_error_id
266 70     70 1 262 {
267             my($self, %args) = @_;
268 70         172  
269 70   66     275 my $error_id = $args{'error_id'};
270 70   100     260 my $msg_class = $args{'msg_class'} || $self->message_class;
271             my $args = $args{'args'} || [];
272 70         285  
273             my $messages_class = $self->messages_class;
274 70 50       668  
    0          
275             if(defined $messages_class->get_message_name($error_id))
276 70         1050 {
277             return $msg_class->new(id => $error_id, args => $args);
278             }
279             elsif($error_id !~ /^\d+$/)
280 0         0 {
281             croak "Unknown error id: $error_id";
282             }
283 0         0  
284             return $msg_class->new(args => $args);
285             }
286              
287             sub select_variant_for_message
288 351     351 1 1148 {
289             my($self, %args) = @_;
290 351         643  
291             my $args = $args{'args'};
292 351 100       933  
293             return $args->{'variant'} if($args->{'variant'});
294 345 100       832  
295             if(defined(my $count = $args->{'count'}))
296 12         57 {
297             return $self->select_variant_for_count(%args, count => $count);
298             }
299 333         1179  
300             return DEFAULT_VARIANT;
301             }
302              
303             sub select_variant_for_count
304 12     12 1 51 {
305             my($self, %args) = @_;
306 12   33     43  
307 12         30 my $locale = $args{'locale'} || $self->locale;
308             my $count = abs($args{'count'});
309              
310             # Possibilities:
311             #
312             # zero
313             # one (singular)
314             # two (dual)
315             # few (paucal)
316             # many
317             # plural
318              
319 12 100       87 # No default judgements on "few" and "many"
    100          
    100          
320             return $count == 0 ? 'zero' :
321             $count == 1 ? 'one' :
322             $count == 2 ? 'two' :
323             'plural';
324             }
325              
326             my %Variant_Cascade =
327             (
328             'zero' => [ 'plural', DEFAULT_VARIANT ],
329             'one' => [ DEFAULT_VARIANT ],
330             'two' => [ 'plural', DEFAULT_VARIANT ],
331             'few' => [ 'plural', DEFAULT_VARIANT ],
332             'many' => [ 'plural', DEFAULT_VARIANT ],
333             'plural' => [ DEFAULT_VARIANT ],
334             );
335              
336             # Trying to avoid repeated anonymous array generation that
337             # might(?) result from using literal [] below
338             my @None;
339              
340             sub variant_cascade
341 376     376 1 1245 {
342 376   100     2036 my($self, %args) = @_;
343             return $Variant_Cascade{$args{'variant'}} ||
344             \@None;
345             }
346              
347             sub localized_message_exists
348 10293     10293 0 17301 {
349             my($self, $name, $locale, $variant) = @_;
350 10293         22009  
351             my $msgs = $self->localized_messages_hash;
352 10293   50     84334  
353             $variant ||= DEFAULT_VARIANT;
354 44     44   387  
  44         150  
  44         53225  
355 10293 100 100     33258 no warnings 'uninitialized';
356             if(exists $msgs->{$name} && exists $msgs->{$name}{$locale})
357 7713 100       17522 {
    50          
358             if(ref $msgs->{$name}{$locale})
359 2572 50       6943 {
360             return $msgs->{$name}{$locale}{$variant} ? 1 : 0;
361             }
362             elsif($variant eq DEFAULT_VARIANT)
363 0         0 {
364             return 1;
365             }
366             }
367 7721         16473  
368             return 0;
369             }
370              
371             sub locales_for_message_name
372 0     0 0 0 {
373             my($self, $name) = @_;
374 0         0  
375             my $msgs = $self->localized_messages_hash;
376 0 0       0  
    0          
377             return wantarray ? () : [] unless(ref $msgs->{$name});
378 0         0  
379 0 0       0 return wantarray ? (sort keys %{$msgs->{$name}}) :
  0         0  
380             [ sort keys %{$msgs->{$name}} ];
381             }
382 7     7 0 88  
383             sub add_localized_message_text { shift->set_localized_message_text(@_) }
384              
385             sub set_localized_message_text
386 102     102 1 467 {
387             my($self, %args) = @_;
388 102         245  
389 102         232 my $id = $args{'id'};
390 102   66     305 my $name = $args{'name'};
391 102         223 my $locale = $args{'locale'} || $self->locale;
392 102         189 my $text = $args{'text'};
393             my $variant = $args{'variant'};
394 102 50       245  
395             croak "Missing new localized message text" unless(defined $text);
396 102 50       360  
397             if($name =~ /[^A-Z0-9_]/)
398 0         0 {
399             croak "Message names must be uppercase and may contain only ",
400             "letters, numbers, and underscores";
401             }
402 102 50 33     488  
    50          
    50          
403             if($id && $name)
404 0 0       0 {
405             unless($name eq $self->messages_class->get_message_name($id))
406 0         0 {
407             croak "The message id '$id' does not match the name '$name'";
408             }
409             }
410             elsif(!defined $name)
411 0 0       0 {
412 0 0       0 croak "Missing message id" unless(defined $id);
413             $name = $self->messages_class->get_message_name($id)
414             or croak "No such message id - '$id'";
415             }
416             elsif(!defined $id)
417 102 50       282 {
418 102 50       361 croak "Missing message name" unless(defined $name);
419             $id = $self->messages_class->get_message_id($name)
420             or croak "No such message name - '$name'";
421             }
422 102 100       406  
423             unless(ref $text eq 'HASH')
424 100         353 {
425             $text = { $locale => $text };
426             }
427 102         336  
428             my $msgs = $self->localized_messages_hash;
429 102         1315  
430             while(my($l, $t) = each(%$text))
431 104 0       310 {
    50          
432             $Debug && warn qq($self - Adding text $name),
433             ($variant ? "($variant)" : ''),
434             qq( [$l] - "$t"\n);
435 104 100       242  
436             if($variant)
437 7 100       30 {
438             if(ref $msgs->{$name}{$l})
439 5         29 {
440             $msgs->{$name}{$l}{$variant} = "$t"; # force stringification
441             }
442             else
443 2         5 {
444             my $existing = $msgs->{$name}{$l};
445 2 50       6  
446             if(defined $existing)
447 0         0 {
448 0         0 $msgs->{$name}{$l} = {};
449             $msgs->{$name}{$l}{DEFAULT_VARIANT()} = $existing;
450             }
451 2         13  
452             $msgs->{$name}{$l}{$variant} = "$t"; # force stringification
453             }
454             }
455             else
456 97 50       330 {
457             if(ref ref $msgs->{$name}{$l})
458 0         0 {
459             $msgs->{$name}{$l}{DEFAULT_VARIANT()} = "$t"; # force stringification
460             }
461             else
462 97         530 {
463             $msgs->{$name}{$l} = "$t"; # force stringification
464             }
465             }
466             }
467 102         392  
468             return $id;
469             }
470              
471             sub import_message_ids
472 2     2 0 29 {
473             my($self) = shift;
474 2 50       6  
475             if($Rose::HTML::Object::Exporter::Target_Class)
476 0         0 {
477             $self->messages_class->import(@_);
478             }
479             else
480 2         10 {
481 2         8 local $Rose::HTML::Object::Exporter::Target_Class = (caller)[0];
482             $self->messages_class->import(@_);
483             }
484             }
485              
486             sub import_error_ids
487 1     1 0 16 {
488             my($self) = shift;
489 1 50       5  
490             @_ = (':all') unless(@_);
491 1 50       4  
492             if($Rose::HTML::Object::Exporter::Target_Class)
493 0         0 {
494             $self->errors_class->import(@_);
495             }
496             else
497 1         19 {
498 1         5 local $Rose::HTML::Object::Exporter::Target_Class = (caller)[0];
499             $self->errors_class->import(@_);
500             }
501             }
502              
503             sub add_localized_message
504 18     18 1 618 {
505             my($self, %args) = @_;
506 18   66     126  
507 18   66     162 my $id = $args{'id'} || $self->generate_message_id;
508 17   33     93 my $name = $args{'name'} || croak "Missing name for new localized message";
509 17         147 my $locale = $args{'locale'} || $self->locale;
510             my $text = $args{'text'};
511 17 100       162  
512             croak "Missing new localized message text" unless(defined $text);
513 16 50       88  
514             if($name =~ /[^A-Z0-9_]/)
515 0         0 {
516             croak "Message names must be uppercase and may contain only ",
517             "letters, numbers, and underscores";
518             }
519 16 100       91  
520             unless(ref $text eq 'HASH')
521 5         29 {
522             $text = { $locale => $text };
523             }
524 16         82  
525 16         147 my $msgs = $self->localized_messages_hash;
526             my $msgs_class = $self->messages_class;
527 16         126  
528             my $const = "${msgs_class}::$name";
529 16 100       140  
530             if(defined &$const)
531 1         198 {
532             croak "A constant or subroutine named $name already exists in the class $msgs_class";
533             }
534 15         96  
535             $msgs_class->add_message($name, $id);
536 15         102  
537             while(my($l, $t) = each(%$text))
538 26 50       79 {
539 26         141 $Debug && warn qq($self - Adding message $name ($l) = "$t"\n);
540             $msgs->{$name}{$l} = "$t"; # force stringification
541             }
542 15         73  
543             return $id;
544             }
545 44     44   511  
  44         135  
  44         25508  
546             use constant NEW_ID_OFFSET => 100_000;
547              
548             our $Last_Generated_Message_Id = NEW_ID_OFFSET;
549             our $Last_Generated_Error_Id = NEW_ID_OFFSET;
550              
551             sub generate_message_id
552 16     16 1 38 {
553             my($self) = shift;
554 16         69  
555 16         150 my $messages_class = $self->messages_class;
556             my $errors_class = $self->errors_class;
557 16         58  
558 16   100     119 my $new_id = $Last_Generated_Error_Id;
559             $new_id++ while($messages_class->message_id_exists($new_id) ||
560             $errors_class->error_id_exists($new_id));
561 16         419  
562             return $Last_Generated_Message_Id = $new_id;
563             }
564              
565             sub generate_error_id
566 3     3 1 7 {
567             my($self) = shift;
568 3         9  
569 3         22 my $errors_class = $self->errors_class;
570             my $messages_class = $self->messages_class;
571 3         18  
572 3   100     19 my $new_id = $Last_Generated_Error_Id;
573             $new_id++ while($errors_class->error_id_exists($new_id) ||
574             $messages_class->message_id_exists($new_id));
575 3         40  
576             return $Last_Generated_Error_Id = $new_id;
577             }
578              
579             sub add_localized_error
580 6     6 1 112 {
581             my($self, %args) = @_;
582 6   66     24  
583 6 100       107 my $id = $args{'id'} || $self->generate_error_id;
584             my $name = $args{'name'} or croak "Missing localized error name";
585 5         12  
586             my $errors_class = $self->errors_class;
587 5         35  
588             my $const = "${errors_class}::$name";
589 5 100       33  
590             if(defined &$const)
591 1         88 {
592             croak "A constant or subroutine named $name already exists in the class $errors_class";
593             }
594 4         17  
595             $errors_class->add_error($name, $id);
596 4         12  
597             return $id;
598             }
599              
600             sub dump_messages
601 0     0 0 0 {
602 0         0 my($self, $code) = @_;
603 0 0       0 my $msgs = $self->localized_messages_hash;
604 0         0 return $code->($msgs) if($code);
605 0         0 require Data::Dumper;
606             return Data::Dumper::Dumper($msgs);
607             }
608              
609             sub get_localized_message_text
610 395     395 1 1735 {
611             my($self, %args) = @_;
612 395         761  
613 395         668 my $id = $args{'id'};
614 395   33     936 my $name = $args{'name'};
615 395   50     907 my $locale = $args{'locale'} || $self->locale;
616 395         679 my $variant = $args{'variant'} || DEFAULT_VARIANT;
617             my $from_class = $args{'from_class'};
618 395   33     875  
619             $from_class ||= (caller)[0];
620 395   33     1544  
621             $name ||= $self->get_message_name($id);
622 395         4510  
623             my $msgs = $self->localized_messages_hash;
624              
625 395         3652 # Try this twice: before and after loading messages
626             foreach my $try (1, 2)
627 44     44   384 {
  44         134  
  44         25415  
628 490 100 100     2146 no warnings 'uninitialized';
629             if(exists $msgs->{$name} && exists $msgs->{$name}{$locale})
630 383 100 100     1044 {
631             if(ref $msgs->{$name}{$locale} && exists $msgs->{$name}{$locale}{$variant})
632 12         53 {
633             return $msgs->{$name}{$locale}{$variant};
634             }
635 371 100       1540  
636             return $msgs->{$name}{$locale} if($variant eq DEFAULT_VARIANT);
637             }
638 139 100       397  
639             last if($try == 2);
640 95         317  
641             $self->load_localized_message($name, $locale, $variant, $from_class);
642             }
643 44         224  
644             return undef;
645             }
646              
647             # ([A-Z0-9_]+) -> ([A-Z0-9_]+) (?: \( \s* (\w[-\w]*) \s* \) )?
648             # ([A-Z0-9_]+) -> ([A-Z0-9_]+)(?:\(\s*([-\w]+)\s*\))?
649             my $Locale_Declaration = qr{^\s* \[% \s* LOCALE \s* (\S+) \s* %\] \s* (?: \#.*)?$}x;
650             my $Start_Message = qr{^\s* \[% \s* START \s+ ([A-Z0-9_]+)(?:\(\s*([-\w]+)\s*\))? \s* %\] \s* (?: \#.*)?$}x;
651             my $End_Message = qr{^\s* \[% \s* END \s+ ([A-Z0-9_]+)(?:\(\s*([-\w]+)\s*\))?? \s* %\] \s* (?: \#.*)?$}x;
652             my $Message_Spec = qr{^ \s* ([A-Z0-9_]+)(?:\(\s*([-\w]+)\s*\))? \s* = \s* "((?:[^"\\]+|\\.)*)" \s* (?: \#.*)? $}x;
653             my $Comment_Or_Blank = qr{^ \s* \# | ^ \s* $}x;
654             my $End_Messages = qr{^=\w|^\s*__END__};
655              
656             my %Data_Pos;
657              
658             sub load_localized_message
659 10292     10292 0 18781 {
660             my($self, $name, $locale, $variant, $from_class) = @_;
661 10292   33     18192  
662             $from_class ||= $self->messages_class;
663 10292 50       19050  
664             if($self->localized_message_exists($name, $locale, $variant))
665 0         0 {
666             return $self->get_localized_message_text(name => $name,
667             locale => $locale,
668             variant => $variant);
669             }
670 44     44   379  
  44         119  
  44         5364  
671 10292         15011 no strict 'refs';
  10292         27316  
672             my $fh = \*{"${from_class}::DATA"};
673 10292 100       21116  
674             if(fileno($fh))
675 944         3625 {
676             local $/ = "\n";
677 944 100       2250  
678             if($Data_Pos{$from_class})
679             {
680 912         8416 # Rewind to the start of the __DATA__ section
681             seek($fh, $Data_Pos{$from_class}, 0);
682             }
683             else
684 32         128 {
685             $Data_Pos{$from_class} = tell($fh);
686             }
687 944         3942  
688             my $text = $self->load_messages_from_fh(fh => $fh,
689             locales => $locale,
690             variants => $variant,
691             names => $name,
692 944 100       4207 force_utf8 => 1);
693             return $text if(defined $text);
694             }
695 44     44   328  
  44         112  
  44         24285  
696             no strict 'refs';
697 10241         14230  
  10241         28747  
698 10241         15070 my @classes = @{"${from_class}::ISA"};
699             my %seen;
700 10241         19571  
701             while(@classes)
702 10197         16772 {
703 10197 50       23255 my $class = pop(@classes);
704             next if($seen{$class}++);
705 10197         21755 #$Debug && warn "$self SEARCHING $class FOR $name ($locale)\n";
706 10197 100       18830 my $msg = $self->load_localized_message($name, $locale, $variant, $class);
707 10157         13607 return $msg if(defined $msg);
  5054         18326  
  10157         27934  
708             push(@classes, grep { !$seen{$_} } @{"${class}::ISA"});
709             }
710 10201         20423  
711             return undef;
712             }
713              
714             sub auto_load_locales
715 4     4 0 31 {
716             my($self_or_class) = shift;
717 4   33     16  
718             my $class = ref($self_or_class) || $self_or_class;
719 4 100       10  
720             if(@_)
721 1 50 33     8 {
  0         0  
722 1         4 my $locales = (@_ == 1 && ref $_[0] eq 'ARRAY') ? [ @{$_[0]} ] : [ @_ ];
723             return $class->_auto_load_locales($locales);
724             }
725 3         13  
726 3 50       70 my $locales = $class->_auto_load_locales;
    100          
727             return wantarray ? @$locales : $locales if(defined $locales);
728 2 100       9  
729             if(my $locales = $ENV{'RHTMLO_LOCALES'})
730 1 50       9 {
731 1         4 $locales = [ split(/\s*,\s*/, $locales) ] unless(ref $locales);
732 1 50       10 $class->_auto_load_locales($locales);
733             return wantarray ? @$locales : $locales;
734             }
735 1 50       5  
736             return wantarray ? () : [];
737             }
738              
739             sub auto_load_messages
740 128     128 1 5759 {
741             my($self_or_class) = shift;
742 128   33     479  
743             my $class = ref($self_or_class) || $self_or_class;
744 128 50       432  
745             if(@_)
746 0         0 {
747             return $class->_auto_load_messages(@_);
748             }
749 128         944  
750 128 50       3527 my $ret = $class->_auto_load_messages;
751             return $ret if(defined $ret);
752 128 100 0     1416  
      33        
      66        
753             if(($ENV{'MOD_PERL'} && (!defined($ENV{'RHTMLO_PRIME_CACHES'}) || $ENV{'RHTMLO_PRIME_CACHES'})) ||
754             $ENV{'RHTMLO_PRIME_CACHES'})
755 1         3 {
756             return $class->_auto_load_messages(1);
757             }
758 127         739  
759             return undef;
760             }
761              
762             sub load_all_messages
763 3     3 1 96 {
764             my($class) = shift;
765 3         6  
766             my %args;
767 3 100       10  
768             if(@_ > 1)
769 1         3 {
770             %args = @_;
771             }
772             else
773 2         5 {
774             $args{'from_class'} = $_[0];
775             }
776 3   66     13  
777             my $from_class = $args{'from_class'} || (caller)[0];
778 44     44   365  
  44         113  
  44         26623  
779 3         6 no strict 'refs';
  3         12  
780             my $fh = \*{"${from_class}::DATA"};
781 3 50       14  
782             if(fileno($fh))
783 3         15 {
784             local $/ = "\n";
785 3 100       8  
786             if($Data_Pos{$from_class})
787             {
788 2         23 # Rewind to the start of the __DATA__ section
789             seek($fh, $Data_Pos{$from_class}, 0);
790             }
791             else
792 1         4 {
793             $Data_Pos{$from_class} = tell($fh);
794             }
795 3         160  
796             my $locales = $class->auto_load_locales;
797 3 50       9  
798 3         10 $Debug && warn "$class - Loading messages from DATA section of $from_class\n";
799             $class->load_messages_from_fh(fh => $fh, locales => $locales, force_utf8 => 1);
800             }
801             }
802              
803             sub load_messages_from_file
804 2     2 1 74 {
805             my($self) = shift;
806 2         4  
807 2 50       10 my %args;
    0          
808             if(@_ == 1)
809 2         9 {
810             $args{'file'} = shift;
811             }
812             elsif(@_ > 1)
813 0 0       0 {
814             croak "Odd number of arguments passed to load_messages_from_file()"
815 0         0 if(@_ % 2 != 0);
816             %args = @_;
817             }
818 2 50       10  
819             my $file = delete $args{'file'} or croak "Missing file argument";
820 2 50       114  
821 2         27 open($args{'fh'}, $file) or croak "Could no open messages file '$file' - $!";
822 2         31 $self->load_messages_from_fh(%args);
823             close($args{'fh'});
824             }
825              
826             sub load_messages_from_fh
827 950     950 0 4221 {
828             my($self, %args) = @_;
829 950         2574  
830             my($fh, $locales, $variants, $msg_names) = @args{qw(fh locales variants names)};
831 950 100       5337  
832             binmode($fh, ':utf8') if($args{'force_utf8'});
833 950 100 66     4491  
    100          
834             if(ref $locales eq 'ARRAY')
835 3 100       10 {
  3         11  
836             $locales = @$locales ? { map { $_ => 1} @$locales } : undef;
837             }
838             elsif($locales && !ref $locales)
839 944         2454 {
840             $locales = { $locales => 1 };
841             }
842 950 50 66     3381  
    100          
843             if(ref $variants eq 'ARRAY')
844 0 0       0 {
  0         0  
845             $variants = @$variants ? { map { $_ => 1} @$variants } : undef;
846             }
847             elsif($variants && !ref $variants)
848 944         2142 {
849             $variants = { $variants => 1 };
850             }
851 950         1482  
852             my $msg_re;
853 950 100       1812  
854             if($msg_names)
855 944 50       1866 {
    0          
    0          
856             if(!ref $msg_names)
857 944         1944 {
858             $msg_names = { $msg_names => 1 };
859             }
860             elsif(ref $msg_names eq 'ARRAY')
861 0         0 {
  0         0  
862             $msg_names = { map { $_ => 1 } @$msg_names };
863             }
864             elsif(ref $msg_names eq 'Regexp')
865 0         0 {
866 0         0 $msg_re = $msg_names;
867             $msg_names = undef;
868             }
869             }
870 950         1590  
871 950         1608 my @text;
872 950         1440 my $in_locale = '';
873 950         1402 my $in_msg = '';
874 950         1418 my $variant = '';
875             my $text = '';
876 950         1728  
877             my $pos = tell($fh);;
878 44     44   369  
  44         142  
  44         25675  
879             no strict 'refs';
880 950         1502  
881             local $_;
882 950         13620  
883             while(<$fh>)
884 32215 100       166120 {
885             last if(/$End_Messages/o);
886              
887             #$Debug && warn "PROC: $_";
888 31301 100 33     198361  
    100 66        
    100          
    100          
    100          
    50          
889             if(/$End_Message/o && (!$2 || $2 eq $in_msg))
890 4 0 33     13 {
      0        
      0        
891             if(!$msg_names || $msg_names->{$in_msg} || ($msg_re && $in_msg =~ /$msg_re/))
892 4         14 {
893             for($text)
894 4         17 {
895 4         29 s/\A(\s*\n)+//;
896             s/(\s*\n)+\z//;
897             }
898              
899             #if($args{'force_utf8'} && !utf8::is_utf8($text))
900             #{
901             # require Encode;
902             # $text = Encode::decode('UTF-8', $text);
903             #}
904 4         21  
905             $self->set_localized_message_text(name => $in_msg,
906             locale => $in_locale,
907             variant => $variant,
908             text => $text);
909             }
910 4         9  
911 4         9 $text = '';
912 4         23 $in_msg = '';
913             $variant = '';
914             }
915             elsif($in_msg)
916 4         38 {
917             $text .= $_;
918             }
919             elsif(/$Locale_Declaration/o)
920 3453         14055 {
921             $in_locale = $1;
922             }
923             elsif(/$Message_Spec/o)
924 16701 100 100     97528 {
      100        
      100        
      100        
      100        
925             if((!$locales || $locales->{$in_locale}) &&
926             (!$variants || $variants->{$2 || DEFAULT_VARIANT}) &&
927             (!$msg_names || $msg_names->{$1}))
928 91         252 {
929 91         186 my $name = $1;
930 91         223 $variant = $2;
931             my $text = $3;
932 91         226  
933             for($text)
934 91         257 {
935 91         292 s/\\n/\n/g;
936             s/\\([^\[])/$1/g;
937             }
938              
939             #if($args{'force_utf8'} && !utf8::is_utf8($text))
940             #{
941             # require Encode;
942             # $text = Encode::decode('UTF-8', $text);
943             #}
944 91         392  
945             $self->set_localized_message_text(name => $name,
946             locale => $in_locale,
947             text => $text,
948 91 100       667 variant => $variant);
949             push(@text, $text) if($msg_names);
950             }
951             }
952             elsif(/$Start_Message/o)
953 4         14 {
954 4         23 $in_msg = $1;
955             $variant = $2;
956             }
957             elsif(!/$Comment_Or_Blank/o)
958 0         0 {
959 0         0 chomp;
960             carp "WARNING: Localized message line not understood: $_";
961             }
962             }
963              
964 950         12566 # Rewind to the starting position
965             seek($fh, $pos, 0);
966 950 50       7756  
967 0         0 return wantarray ? @text : $text[0];
968             return;
969             }
970              
971             sub load_messages_from_string
972 1     1 0 36 {
973             my($self) = shift;
974 1 50       7  
975             my %args = @_ == 1 ? (string => shift) : @_;
976 1         581  
977             require IO::String;
978 1         4092  
979             $args{'fh'} = IO::String->new(delete $args{'string'});
980 1         59  
981             return $self->load_messages_from_fh(%args);
982             }
983 44     44   23523  
  44         699  
  44         314  
984             use utf8; # The __END__ section contains UTF-8 text
985              
986             1;
987              
988             __END__
989              
990             =encoding utf-8
991              
992             =head1 NAME
993              
994             Rose::HTML::Object::Message::Localizer - Message localizer class.
995              
996             =head1 SYNOPSIS
997              
998             # The localizer for a given class or object is usually accessibly
999             # via the "localizer" class or object method.
1000              
1001             $localizer = Rose::HTML::Object->localizer;
1002             $localizer = $object->localizer;
1003              
1004             ...
1005              
1006             # The localizer is rarely used directly. More often, it is
1007             # subclassed so you can provide your own alternate source for
1008             # localized messages. See the LOCALIZATION section of the
1009             # Rose::HTML::Objects documentation for more information.
1010              
1011             package My::HTML::Object::Message::Localizer;
1012              
1013             use base qw(Rose::HTML::Object::Message::Localizer);
1014             ...
1015             sub get_localized_message_text
1016             {
1017             my($self) = shift;
1018              
1019             # Get localized message text from the built-in sources
1020             my $text = $self->SUPER::get_localized_message_text(@_);
1021              
1022             unless(defined $text)
1023             {
1024             my %args = @_;
1025              
1026             # Get message text from some other source
1027             ...
1028             }
1029              
1030             return $text;
1031             }
1032              
1033             =head1 DESCRIPTION
1034              
1035             L<Rose::HTML::Object::Message::Localizer> objects are responsible for managing localized L<messages|Rose::HTML::Object::Messages> and L<errors|Rose::HTML::Object::Errors> which are identified by integer ids and symbolic constant names. See the L<Rose::HTML::Object::Messages> and L<Rose::HTML::Object::Errors> documentation for more infomation on messages and errors.
1036              
1037             In addition to collecting and providing access to messages and errors, L<Rose::HTML::Object::Message::Localizer> objects also provide appropriately localized text for each message and error.
1038              
1039             This class inherits from, and follows the conventions of, L<Rose::Object>. See the L<Rose::Object> documentation for more information.
1040              
1041             =head2 MESSAGES AND ERRORS
1042              
1043             L<Messages|Rose::HTML::Object::Messages> and L<errors|Rose::HTML::Object::Errors> are stored and tracked separately, but are intimately related. Both entities have integer ids which may be imported as symbolic constants, but only messages have associated localized text.
1044              
1045             The integer message and error ids are convenient, compact, and easily comparable. Using these constants in your code allows you to refer to messages and errors in a way that is divorced from any actual message text. For example, if you wanted to subclass L<Rose::HTML::Form::Field::Integer> and do something special in response to "invalid integer" errors, you could do this:
1046              
1047             package My::HTML::Form::Field::Integer;
1048              
1049             use base 'Rose::HTML::Form::Field::Integer';
1050              
1051             # Import the symbol for the "invalid integer" error
1052             use Rose::HTML::Object::Errors qw(NUM_INVALID_INTEGER);
1053              
1054             sub validate
1055             {
1056             my($self) = shift;
1057              
1058             my $ret = $self->SUPER::validate(@_);
1059              
1060             unless($ret)
1061             {
1062             if($self->error_id == NUM_INVALID_INTEGER)
1063             {
1064             ... # do something here
1065             }
1066             }
1067              
1068             return $ret;
1069             }
1070              
1071             Note how detecting the exact error did not require regex-matching against error message text or anything similarly unmaintainable.
1072              
1073             When it comes time to display appropriate localized message text for the C<NUM_INVALID_INTEGER> error, the aptly named L<message_for_error_id|/message_for_error_id> method is called. This method exists in the localizer, and also in L<Rose::HTML::Object|Rose::HTML::Object/message_for_error_id> and L<Rose::HTML::Form::Field|Rose::HTML::Form::Field/message_for_error_id>. The localizer's incarnation of the method is usually only called if the other two are not available (e.g., in the absence of any HTML object or field). The mapping between error ids and message ids is direct by default (i.e., error id 123 maps to message id 123) but can be entirely aribtrary.
1074              
1075             =head2 LOCALIZED TEXT
1076              
1077             Broadly speaking, localized text can come from anywhere. See the L<localization|Rose::HTML::Objects/LOCALIZATION> section of the L<Rose::HTML::Objects> documentaton for a description of how to create your own localizer subclass that loads localized message text from the source of your choosing.
1078              
1079             The base L<Rose::HTML::Object::Message::Localizer> class reads localized text from the C<__DATA__> sections of Perl source code files and stores it in memory within the localizer object itself. Such text is read in en masse when the L<load_all_messages|/load_all_messages> method is called, or on demand in response to requests for localized text. The L<auto_load_messages|/auto_load_messages> flag may be used to distinguish between the two policies. Here's an example C<__DATA__> section and L<load_all_messages|/load_all_messages> call (from the L<Rose::HTML::Form::Field::Integer> source code):
1080              
1081             if(__PACKAGE__->localizer->auto_load_messages)
1082             {
1083             __PACKAGE__->localizer->load_all_messages;
1084             }
1085              
1086             1;
1087              
1088             __DATA__
1089              
1090             [% LOCALE en %]
1091              
1092             NUM_INVALID_INTEGER = "[label] must be an integer."
1093             NUM_INVALID_INTEGER_POSITIVE = "[label] must be a positive integer."
1094             NUM_NOT_POSITIVE_INTEGER = "[label] must be a positive integer."
1095              
1096             [% LOCALE de %]
1097              
1098             NUM_INVALID_INTEGER = "[label] muß eine Ganzzahl sein."
1099             NUM_INVALID_INTEGER_POSITIVE = "[label] muß eine positive Ganzzahl sein."
1100             NUM_NOT_POSITIVE_INTEGER = "[label] muß eine positive Ganzzahl sein."
1101              
1102             [% LOCALE fr %]
1103              
1104             NUM_INVALID_INTEGER = "[label] doit être un entier."
1105             NUM_INVALID_INTEGER_POSITIVE = "[label] doit être un entier positif."
1106             NUM_NOT_POSITIVE_INTEGER = "[label] doit être un entier positif."
1107              
1108             The messages for each locale are set off by C<LOCALE> directives surrounded by C<[%> and C<%]>. All messages until the next such declaration are stored under the specified locale.
1109              
1110             Localized text is provided in double-quoted strings to the right of symbolic L<messages|Rose::HTML::Object::Messages> constant names.
1111              
1112             Placeholders are replaced with text provided at runtime. Placeholder names are surrounded by square brackets. They must start with C<[a-zA-Z]> and may contain only characters that match C<\w>. For an example, see the C<[label]> placeholders in the mssage text above. A C<@> prefix is allowed to specify that the placeholder value is expected to be a reference to an array of values.
1113              
1114             SOME_MESSAGE = "A list of values: [@values]"
1115              
1116             In such a case, the values are joined with ", " to form the text that replaces the placeholder.
1117              
1118             Embedded double quotes in message text must be escaped with a backslash. Embedded newlines may be included using a C<\n> sequence. Literal opening square brackets must be backslash-escaped: C<\[>. Literal backslashes must be doubled: C<\\>. Example:
1119              
1120             SOME_MESSAGE = "Here\[]:\nA backslash \\ and some \"embedded\" double quotes"
1121              
1122             The resulting text:
1123              
1124             Here[]:
1125             A backslash \ and some "embedded" double quotes
1126              
1127             There's also a multi-line format for longer messages:
1128              
1129             [% START SOME_MESSAGE %]
1130             This message has multiple lines.
1131             Here's another one.
1132             [% END SOME_MESSAGE %]
1133              
1134             Leading and trailing spaces and newlines are removed from text provided in the multi-line format.
1135              
1136             Blank lines and any lines beginning with a C<#> character are skipped.
1137              
1138             =head3 VARIANTS
1139              
1140             Any L<message|Rose::HTML::Object::Messages> constant name may be followed immediately by a variant name within parentheses. Variant names may contain only the characters C<[A-Za-z0-9_-]>. If no variant is provided, the variant is assumed to be C<default>. In other words, this:
1141              
1142             SOME_MESSAGE(default) = "..."
1143              
1144             is equivalent to this:
1145              
1146             SOME_MESSAGE = "..."
1147              
1148             Before going any further, the key thing to remember about variants is that you can ignore them entirely, if you wish. Don't use any variants in your message text and don't specify any variants when asking for localized message text and you can pretend that they do not exist.
1149              
1150             With that out of the way, there are some good reasons why you might want to use variants. But first, let's examine how they work. We've already seen the syntax for specifying variants using the built-in localized message text format. The next piece of the puzzle is the ability to specify a particular variant for a message. That can be done either explicitly or indirectly. First, the explicit approach.
1151              
1152             Requesting a variant explicitly is done using the special C<variant> L<message argument|Rose::HTML::Object::Message::Localized/args>. Example:
1153              
1154             $field->error_id($id, { variant => 'foo' });
1155              
1156             Aside from indicating the message variant, the C<variant> argument is treated just like any other. That is, if you happen to have a placeholder named C<variant>, then the value will be subtituted for it. (This being the case, it's usually a good idea to avoid using C<variant> as a placeholder name.)
1157              
1158             If no explicit C<variant> is specified, the L<select_variant_for_message|/select_variant_for_message> method is called to select an appropriate variant. The default implementation of this method returns the L<default variant|/default_variant> most of the time. But if there is a L<message argument|Rose::HTML::Object::Message::Localized/args> named C<count>, then the L<select_variant_for_count|/select_variant_for_count> method is called in order to select the variant.
1159              
1160             This leads to the primary intended use of variants: pluralization. English has relatively simple pluralization rules, but other languages have special grammar for not just singular and plural, but also "dual," and sometimes even "many" and "few." The pluralization variant names expected by the default implementation of L<select_variant_for_count|/select_variant_for_count> roughly follow the CLDR guidelines:
1161              
1162             L<http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html>
1163              
1164             with the exception that C<plural> is used in place of C<other>. (Variants are a general purpose mechanism, whereas the context of pluralization is implied in the case of the CLDR terms. A variant named C<other> has no apparent connection to pluralization.)
1165              
1166             The default implementation of L<select_variant_for_count|/select_variant_for_count> (sanely) makes no judgements about "few" or "many," but does return C<zero> for a C<count> of 0, C<one> for 1, C<two> for 2, and C<plural> for all other values of C<count>.
1167              
1168             But since English has no special pluralization grammar for two items, how is this expected to work in the general case? The answer is the so-called "L<variant cascade|/variant_cascade>." If the desired variant is not available for the specified message in the requested locale, then the L<variant_cascade|/variant_cascade> method is called. It is passed the locale, the desired variant, the message itself, and the message arguments. It returns a list of other variants to try based on the arguments it was passed.
1169              
1170             The default implementation of L<variant_cascade|/variant_cascade> follows simple English-centric rules, cascading directly to C<plural> except in the case of the C<one> variant, and appending the L<default variant|/default_variant> to the end of all cascades.
1171              
1172             (Incidentally, there is also a L<locale cascade|/locale_cascade>. The L<localize_message|/localize_message> method uses a nested loop: for each locale, for each variant, look for message text. See the L<localize_message|/localize_message> documentation for more information.)
1173              
1174             Here's an example using variants. (Please forgive the poor translations. I don't speak French. Corrections welcome!) First, the message text:
1175              
1176             [% LOCALE en %]
1177              
1178             FIELD_ERROR_TOO_MANY_DAYS = "Too many days."
1179             FIELD_ERROR_TOO_MANY_DAYS(one) = "One day is too many."
1180             FIELD_ERROR_TOO_MANY_DAYS(two) = "Two days is too many."
1181             FIELD_ERROR_TOO_MANY_DAYS(few) = "[count] days is too many (few)."
1182             FIELD_ERROR_TOO_MANY_DAYS(many) = "[count] days is too many (many)."
1183             FIELD_ERROR_TOO_MANY_DAYS(plural) = "[count] days is too many."
1184              
1185             [% LOCALE fr %]
1186              
1187             FIELD_ERROR_TOO_MANY_DAYS = "Trop de jours."
1188             FIELD_ERROR_TOO_MANY_DAYS(one) = "Un jour est un trop grand nombre."
1189             FIELD_ERROR_TOO_MANY_DAYS(plural) = "[count] jours est un trop grand nombre."
1190              
1191             Now some examples of variant selection:
1192              
1193             use My::HTML::Object::Errors qw(FIELD_ERROR_TOO_MANY_DAYS)l
1194             ...
1195              
1196             $id = FIELD_ERROR_TOO_MANY_DAYS; # to make for shorter lines below
1197              
1198             $field->locale('en');
1199              
1200             $field->error_id($id, { count => 0 });
1201              
1202             # No explicit variant given. The select_variant_for_count() called
1203             # and returns variant "zero". No "zero" variant found for this
1204             # message in locale "en", so the variant_cascade() containing
1205             # ('plural', 'default') is considered, in that order. A "plural"
1206             # variant is found.
1207             print $field->error; # "0 days is too many."
1208              
1209             $field->error_id($id, { count => 2 });
1210              
1211             # No explicit variant given. The select_variant_for_count() called and
1212             # returns variant "two". That message variant is found in locale "en"
1213             print $field->error; # "Two days is too many."
1214              
1215             $field->error_id($id, { count => 3, variant => 'few' });
1216              
1217             # Explicit variant given. That message variant is found in locale "en"
1218             print $field->error; # "3 days is too many (few)."
1219              
1220             $field->locale('fr');
1221              
1222             $field->error_id($id, { count => 0 });
1223              
1224             # No explicit variant given. The select_variant_for_count() called
1225             # and returns variant "zero". No "zero" variant found for this
1226             # message in locale "fr", so the variant_cascade() containing
1227             # ('plural', 'default') is considered, in that order. A "plural"
1228             # variant is found.
1229             print $field->error; # "0 jours est un trop grand nombre."
1230              
1231             $field->error_id($id, { count => 3, variant => 'few' });
1232              
1233             # Explicit variant given. No "few" variant found for this message
1234             # in locale "fr", so the variant_cascade() containing ('plural',
1235             # 'default') is considered, in that order. A "plural" variant is
1236             # found.
1237             print $field->error; # "3 jours est un trop grand nombre."
1238              
1239             I hope you get the idea. Remember that what's described above is merely the default implementation. You are fully expected to override any and all public methods in the localizer in you L<private library|Rose::HTML::Objects/"PRIVATE LIBRARIES"> to alter their behavior. An obvious choice is the L<variant_cascade|/variant_cascade> method, which you might want to override to provide more sensible per-locale cascades, replacing the default English-centric rules.
1240              
1241             And even if you don't plan to use the variant system at all, you might want to override L<select_variant_for_message|/select_variant_for_message> to unconditionally return the L<default variant|/default_variant>, which will eliminate the special treatment of message arguments named C<count> and C<variant>.
1242              
1243             =head3 CUSTOMIZATION
1244              
1245             The implementation of localized message storage described above exists primarily because it's the most convenient way to store and distribute the localized messages that ship with the L<Rose::HTML::Objects> module distribution. For a real application, it may be preferable to store localized text elsewhere.
1246              
1247             The easiest way to do this is to create your own L<Rose::HTML::Object::Message::Localizer> subclass and override the L<get_localized_message_text|/get_localized_message_text> method, or any other method(s) you desire, and provide your own implementation of localized message storage and retrieval.
1248              
1249             You must then ensure that your new localizer subclass is actually used by all of your HTML objects. You can, of course, set the L<localizer|Rose::HTML::Object/localizer> attribute directly, but a much more comprehensive way to customize your HTML objects is by creating your own, private family tree of L<Rose::HTML::Object>-derived classes. Please see the L<private libraries|Rose::HTML::Objects/"PRIVATE LIBRARIES"> section of the L<Rose::HTML::Objects> documentation for more information.
1250              
1251             =head2 LOCALES
1252              
1253             Localization is done based on a "locale", which is an arbitrary string containing one or more non-space characters. The locale string must evaluate to a true value (i.e., the string "0" is not allowed as a locale). The default set of locales used by the L<Rose::HTML::Objects> modules are lowercase two-letter language codes:
1254              
1255             LOCALE LANGUAGE
1256             ------ --------
1257             en English
1258             de German
1259             fr French
1260             bg Bulgarian
1261              
1262             Localized versions of all built-in messages and errors are provided for all of these locales.
1263              
1264             =head1 CLASS METHODS
1265              
1266             =over 4
1267              
1268             =item B<auto_load_messages [BOOL]>
1269              
1270             Get or set a boolean value indicating whether or not localized message text should be automatically loaded from classes that call their localizer's L<load_all_messages|/load_all_messages> method. The default value is true if either of the C<MOD_PERL> or C<RHTMLO_PRIME_CACHES> environment variables are set to a true value, false otherwise.
1271              
1272             =item B<default_locale [LOCALE]>
1273              
1274             Get or set the default L<locale|/locale> used by objects of this class. Defaults to "en".
1275              
1276             =item B<default_locale_cascade [PARAMS]>
1277              
1278             Get or set the default locale cascade. PARAMS are L<locale|/"LOCALES">/arrayref pairs. Each referenced array contains a list of locales to check, in the order specified, when message text is not available in the desired locale. There is one special locale name, C<default>, that's used if no locale cascade exists for a particular locale. The default locale cascade is:
1279              
1280             default => [ 'en' ]
1281              
1282             That is, if message text is not available in the desired locale, C<en> text will be returned instead (assuming it exists).
1283              
1284             This method returns the default locale cascade as a reference to a hash of locale/arrayref pairs (in scalar context) or a list of locale/arrayref pairs (in list context).
1285              
1286             =item B<load_all_messages [PARAMS]>
1287              
1288             Load all localized message text from the C<__DATA__> section of the class specified by PARAMS name/value pairs. Valid PARAMS are:
1289              
1290             =over 4
1291              
1292             =item B<from_class CLASS>
1293              
1294             The name of the class from which to load localized message text. Defaults to the name of the class from which this method was called.
1295              
1296             =back
1297              
1298             =back
1299              
1300             =head1 CONSTRUCTOR
1301              
1302             =over 4
1303              
1304             =item B<new [PARAMS]>
1305              
1306             Constructs a new L<Rose::HTML::Object::Message::Localizer> object based on PARAMS, where PARAMS are
1307             name/value pairs. Any object method is a valid parameter name.
1308              
1309             =back
1310              
1311             =head1 OBJECT METHODS
1312              
1313             =over 4
1314              
1315             =item B<add_localized_error PARAMS>
1316              
1317             Add a new localized error message. PARAMS are name/value pairs. Valid PARAMS are:
1318              
1319             =over 4
1320              
1321             =item B<id ID>
1322              
1323             An integer L<error|Rose::HTML::Object::Errors> id. Error ids from 0 to 29,999 are reserved for built-in errors. Negative error ids are reserved for internal use. Please use error ids 30,000 or higher for your errors. If omitted, the L<generate_error_id|/generate_error_id> method will be called to generate a value.
1324              
1325             =item B<name NAME>
1326              
1327             An L<error|Rose::HTML::Object::Errors> name. This parameter is required. Error names may contain only the characters C<[A-Z0-9_]> and must be unique among all error names.
1328              
1329             =back
1330              
1331             =item B<add_localized_message PARAMS>
1332              
1333             Add a new localized message. PARAMS are name/value pairs. Valid PARAMS are:
1334              
1335             =over 4
1336              
1337             =item B<id ID>
1338              
1339             An integer L<message|Rose::HTML::Object::Messages> id. Message ids from 0 to 29,999 are reserved for built-in messages. Negative message ids are reserved for internal use. Please use message ids 30,000 or higher for your messages. If omitted, the L<generate_message_id|/generate_message_id> method will be called to generate a value.
1340              
1341             =item B<name NAME>
1342              
1343             A L<message|Rose::HTML::Object::Messages> name. This parameter is required. Message names may contain only the characters C<[A-Z0-9_]> and must be unique among all message names.
1344              
1345             =back
1346              
1347             =item B<default_variant>
1348              
1349             Returns the name of the default variant: C<default>. See the L<variants|/VARIANTS> subsection of the L<localized text|/"LOCALIZED TEXT"> section above for more information on variants.
1350              
1351             =item B<error_class [CLASS]>
1352              
1353             Get or set the name of the L<Rose::HTML::Object::Error>-derived class used to store each error. The default value is L<Rose::HTML::Object::Error>. To change the default, override the C<init_error_class> method in your subclass and return a different class name.
1354              
1355             =item B<errors_class [CLASS]>
1356              
1357             Get or set the name of the L<Rose::HTML::Object::Errors>-derived class used to store and track error ids and symbolic constant names. The default value is L<Rose::HTML::Object::Errors>. To change the default, override the C<init_errors_class> method in your subclass and return a different class name.
1358              
1359             =item B<locale [LOCALE]>
1360              
1361             Get or set the locale assumed by the localizer in the absence of an explicit locale argument. Defaults to the value returned by the L<default_locale|/default_locale> class method.
1362              
1363             =item B<message_class [CLASS]>
1364              
1365             Get or set the name of the L<Rose::HTML::Object::Message>-derived class used to store each message. The default value is L<Rose::HTML::Object::Message::Localized>. To change the default, override the C<init_message_class> method in your subclass and return a different class name.
1366              
1367             =item B<messages_class [CLASS]>
1368              
1369             Get or set the name of the L<Rose::HTML::Object::Messages>-derived class used to store and track message ids and symbolic constant names. The default value is L<Rose::HTML::Object::Messages>. To change the default, override the C<init_messages_class> method in your subclass and return a different class name.
1370              
1371             =item B<generate_error_id>
1372              
1373             Returns a new integer L<error|Rose::HTML::Object::Errors> id. This method will not return the same value more than once.
1374              
1375             =item B<generate_message_id>
1376              
1377             Returns a new integer L<message|Rose::HTML::Object::Messages> id. This method will not return the same value more than once.
1378              
1379             =item B<get_error_id NAME>
1380              
1381             This method is a proxy for the L<errors_class|/errors_class>'s L<get_error_id|Rose::HTML::Object::Errors/get_error_id> method.
1382              
1383             =item B<get_error_name ID>
1384              
1385             This method is a proxy for the L<errors_class|/errors_class>'s L<get_error_name|Rose::HTML::Object::Errors/get_error_name> method.
1386              
1387             =item B<get_localized_message_text PARAMS>
1388              
1389             Returns localized message text based on PARAMS name/value pairs. Valid PARAMS are:
1390              
1391             =over 4
1392              
1393             =item B<id ID>
1394              
1395             An integer L<message|Rose::HTML::Object::Messages> id. If a C<name> is not passed, then the name corresponding to this message id will be looked up using the L<get_message_name|/get_message_name> method.
1396              
1397             =item B<name NAME>
1398              
1399             The L<message|Rose::HTML::Object::Messages> name. If this parameter is not passed, then the C<id> parameter must be passed.
1400              
1401             =item B<locale LOCALE>
1402              
1403             The L<locale|/LOCALES> of the localized message text. Defaults to the localizer's L<locale()|/locale> if omitted.
1404              
1405             =item B<from_class CLASS>
1406              
1407             The name of the class from which to attempt to L<load the localized message text|/"LOCALIZED TEXT">. If omitted, it defaults to the name of the package from which this method was called.
1408              
1409             =back
1410              
1411             =item B<get_message_id NAME>
1412              
1413             This method is a proxy for the L<messages_class|/messages_class>'s L<get_message_id|Rose::HTML::Object::Messages/get_message_id> method.
1414              
1415             =item B<get_message_name ID>
1416              
1417             This method is a proxy for the L<messages_class|/messages_class>'s L<get_message_name|Rose::HTML::Object::Messages/get_message_name> method.
1418              
1419             =item B<load_messages_from_file [ FILE | PARAMS ]>
1420              
1421             Load localized message text, in the format described in the L<LOCALIZED TEXT|/"LOCALIZED TEXT"> section above, from a file on disk. Note that this method only loads message I<text>. The message ids must already exist in the L<messages_class|/messages_class>.
1422              
1423             If a single FILE argument is passed, it is taken as the value for the C<file> parameter. Otherwise, PARAMS name/value pairs are expected. Valid PARAMS are:
1424              
1425             =over 4
1426              
1427             =item B<file PATH>
1428              
1429             The path to the file. This parameter is required.
1430              
1431             =item B<locales [ LOCALE | ARRAYREF ]>
1432              
1433             A L<locale|/"LOCALES"> or a reference to an array of locales. If provided, only message text for the specified locales will be loaded. If omitted, all locales will be loaded.
1434              
1435             =item B<names [ NAME | ARRAYREF | REGEX ]>
1436              
1437             Only load text for the specified messages. Pass either a single message NAME, a reference to an array of names, or a regular expression that matches the names of the messages you want to load.
1438              
1439             =back
1440              
1441             =item B<locale [LOCALE]>
1442              
1443             Get or set the L<locale|/"LOCALES"> of this localizer. This locale is used by several methods when a locale is not explicitly provided. The default value is determined by the L<default_locale|/default_locale> class method.
1444              
1445             =item B<locale_cascade [PARAMS]>
1446              
1447             Get or set the locale cascade. PARAMS are L<locale|/"LOCALES">/arrayref pairs. Each referenced array contains a list of locales to check, in the order specified, when message text is not available in the desired locale. There is one special locale name, C<default>, that's used if no locale cascade exists for a particular locale. The default locale cascade is determined by the L<default_locale_cascade|/default_locale_cascade> class method.
1448              
1449             This method returns the locale cascade as a reference to a hash of locale/arrayref pairs (in scalar context) or a list of locale/arrayref pairs (in list context).
1450              
1451             =item B<localize_message PARAMS>
1452              
1453             Localize a message, returning the appropriately localized and processed message text. Valid PARAMS name/value pairs are:
1454              
1455             =over 4
1456              
1457             =item B<args HASHREF>
1458              
1459             A reference to a hash of L<message arguments|Rose::HTML::Object::Message::Localized/args>. If omitted, the C<message>'s L<args|Rose::HTML::Object::Message::Localized/args> are used.
1460              
1461             =item B<locale LOCALE>
1462              
1463             The locale. If omitted, the C<message>'s L<locale|Rose::HTML::Object::Message::Localized/locale> is used.
1464              
1465             =item B<message MESSAGE>
1466              
1467             The L<Rose::HTML::Object::Message>-derived message object. This parameter is required.
1468              
1469             =item B<variant VARIANT>
1470              
1471             The message L<variant|/"VARIANTS">. If omitted, the L<select_variant_for_message|/select_variant_for_message> method is called, passing the C<message> L<id|Rose::HTML::Object::Message/id>, C<args>, and C<locale>.
1472              
1473             =back
1474              
1475             This method performs a nested loop to search for localized message text: for each locale (including any L<locale_cascade|/locale_cascade>), for each variant (including any L<variant_cascade|/variant_cascade>), for each parent L<field|Rose::HTML::Form::Field/parent_field>, L<form|Rose::HTML::Form::Field/parent_form>, or generic parent L<object|Rose::HTML::Object/parent> (considered in that order), look for message text by calling the L<get_localized_message_text|/get_localized_message_text> method.
1476              
1477             =item B<message_for_error_id PARAMS>
1478              
1479             Given an L<error|Rose::HTML::Object::Errors> id, return the corresponding L<message_class|/message_class> object. The default implementation simply looks for a message with the same integer id as the error. Valid PARAMS name/value pairs are:
1480              
1481             =over 4
1482              
1483             =item B<error_id ID>
1484              
1485             The integer error id. This parameter is required.
1486              
1487             =item B<args HASHREF>
1488              
1489             A reference to a hash of name/value pairs to be used as the L<message arguments|Rose::HTML::Object::Message/args>.
1490              
1491             =back
1492              
1493             =item B<parent [OBJECT]>
1494              
1495             Get or set a weakened reference to the localizer's parent object.
1496              
1497             =item B<select_variant_for_count PARAMS>
1498              
1499             Select and return a L<variant|/"VARIANTS"> name based on PARAMS name/value pairs. Valid PARAMS are:
1500              
1501             =over 4
1502              
1503             =item B<args HASHREF>
1504              
1505             A reference to a hash of L<message arguments|Rose::HTML::Object::Message::Localized/args>.
1506              
1507             =item B<count INTEGER>
1508              
1509             The count for which to select a variant. This parameter is required.
1510              
1511             =item B<locale LOCALE>
1512              
1513             The L<locale|/LOCALES> of the localized message text. Defaults to the localizer's L<locale()|/locale> if omitted.
1514              
1515             =back
1516              
1517             The default implementation looks only at the C<count> parameter and returns the following values based on it (the C<*> below means "any other value"):
1518              
1519             count variant
1520             ----- -------
1521             0 zero
1522             1 one
1523             2 two
1524             * plural
1525              
1526             See the L<variants|/VARIANTS> section for more information on this and other variant-related methods
1527              
1528             =item B<select_variant_for_message PARAMS>
1529              
1530             Select and return a L<variant|/"VARIANTS"> name based on PARAMS name/value pairs. Valid PARAMS are:
1531              
1532             =over 4
1533              
1534             =item B<args HASHREF>
1535              
1536             A reference to a hash of L<message arguments|Rose::HTML::Object::Message::Localized/args>.
1537              
1538             =item B<id MESSAGEID>
1539              
1540             The L<message id|Rose::HTML::Object::Messages>.
1541              
1542             =item B<locale LOCALE>
1543              
1544             The L<locale|/LOCALES> of the localized message text. Defaults to the localizer's L<locale()|/locale> if omitted.
1545              
1546             =back
1547              
1548             If C<args> contains a C<count> parameter, then the L<select_variant_for_count|/select_variant_for_count> method is called, passing all arguments plus the C<count> value as its own parameter, and the variant it returns is returned from this method.
1549              
1550             If C<args> contains a C<variant> parameter, then the value of that parameter is returned.
1551              
1552             Otherwise, the L<default_variant|/default_variant> is returned.
1553              
1554             =item B<set_localized_message_text PARAMS>
1555              
1556             Set the localized text for a message. Valid PARAMS name/value pairs are:
1557              
1558             =over 4
1559              
1560             =item B<id ID>
1561              
1562             An integer L<message|Rose::HTML::Object::Messages> id. If a C<name> is not passed, then the name corresponding to this message id will be looked up using the L<get_message_name|/get_message_name> method.
1563              
1564             =item B<name NAME>
1565              
1566             The L<message|Rose::HTML::Object::Messages> name. If this parameter is not passed, then the C<id> parameter must be passed.
1567              
1568             =item B<locale LOCALE>
1569              
1570             The L<locale|/LOCALES> of the localized message text. Defaults to the localizer's L<locale|/locale>.
1571              
1572             =item B<text TEXT>
1573              
1574             The localized message text.
1575              
1576             =item B<variant VARIANT>
1577              
1578             The message variant, if any. See the L<LOCALIZED TEXT|/"LOCALIZED TEXT"> section above for more information about variants.
1579              
1580             =back
1581              
1582             =item B<variant_cascade [PARAMS]>
1583              
1584             Return a reference to an array of L<variant|/VARIANTS> names under which to look for localized text, assuming the requested variant is not available in the context specified in PARAMS name/value pairs. Valid params are:
1585              
1586             =over 4
1587              
1588             =item B<args HASHREF>
1589              
1590             A reference to a hash of L<message arguments|Rose::HTML::Object::Message::Localized/args>.
1591              
1592             =item B<locale LOCALE>
1593              
1594             The L<locale|/LOCALES> of the desired localized message text.
1595              
1596             =item B<message MESSAGE>
1597              
1598             The L<Rose::HTML::Object::Message>-derived message object.
1599              
1600             =item B<variant VARIANT>
1601              
1602             The originally requested message L<variant|/"VARIANTS">.
1603              
1604             =back
1605              
1606             The default implementation looks only at the C<variant> parameter and returns references to arrays containing the following variant lists based on it:
1607              
1608             variant variant cascade
1609             ------- ---------------
1610             zero plural, default
1611             one default
1612             two plural, default
1613             few plural, default
1614             many plural, default
1615             plural default
1616              
1617             The array references returned should be treated as read-only.
1618              
1619             =back
1620              
1621             =head1 AUTHOR
1622              
1623             John C. Siracusa (siracusa@gmail.com)
1624              
1625             =head1 LICENSE
1626              
1627             Copyright (c) 2010 by John C. Siracusa. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.