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   86884 use strict;
  44         101  
  44         1706  
4              
5 44     44   298 use Carp;
  44         79  
  44         2725  
6 44     44   20751 use Clone::PP();
  44         39487  
  44         1163  
7 44     44   321 use Scalar::Util();
  44         93  
  44         783  
8              
9 44     44   11646 use Rose::HTML::Object::Errors();
  44         149  
  44         1215  
10 44     44   294 use Rose::HTML::Object::Messages();
  44         88  
  44         1082  
11              
12 44     44   217 use base 'Rose::Object';
  44         94  
  44         24547  
13              
14             our $VERSION = '0.606';
15              
16             our $Debug = 0;
17              
18 44     44   12316 use constant DEFAULT_VARIANT => 'default';
  44         100  
  44         4730  
19              
20             #
21             # Object data
22             #
23              
24             use Rose::Object::MakeMethods::Generic
25             (
26 44         846 '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   23322 );
  44         303057  
40              
41             #
42             # Class data
43             #
44              
45             use Rose::Class::MakeMethods::Generic
46             (
47 44         374 inheritable_hash => 'default_locale_cascade',
48 44     44   26983 );
  44         94  
49              
50             use Rose::Class::MakeMethods::Generic
51             (
52 44         272 inheritable_scalar =>
53             [
54             'default_locale',
55             '_auto_load_messages',
56             '_auto_load_locales',
57             ],
58 44     44   10054 );
  44         114  
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 416 sub init_localized_messages_hash { {} }
74              
75             sub init_locale_cascade
76             {
77 26     26 0 339 my($self) = shift;
78 26   33     208 my $class = ref($self) || $self;
79 26         209 return $class->default_locale_cascade;
80             }
81              
82             sub locale_cascade
83             {
84 704     704 1 1217 my($self) = shift;
85              
86 704   66     3940 my $hash = $self->{'locale_cascade'} ||= ref($self)->init_locale_cascade;
87              
88 704 50       2024 if(@_)
89             {
90 704 50       1460 if(@_ == 1)
    0          
91             {
92 704         2964 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 235714 my($self) = shift;
110 33   33     151 my $class = ref($self) || $self;
111 33         323 return $class->default_locale;
112             }
113              
114 26     26 0 415 sub init_messages_class { 'Rose::HTML::Object::Messages' }
115 36     36 0 3486 sub init_message_class { 'Rose::HTML::Object::Message::Localized' }
116 9     9 0 136 sub init_errors_class { 'Rose::HTML::Object::Errors' }
117 28     28 0 888 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 2218     2218 1 12527 my($self, %args) = @_;
131              
132 2218         6638 my $message = $args{'message'};
133              
134 2218 50 33     12530 return $message unless($message->can('text') && $message->can('id'));
135 2218 100       5727 return $message->text if($message->is_custom);
136              
137 351         716 my $parent = $message;
138              
139 351 50       1284 if($parent->can('parent'))
140             {
141 351         1432 $parent = $parent->parent;
142             }
143              
144 351 100 100     2319 if($parent && $parent->isa('Rose::HTML::Object::Error'))
145             {
146 132         375 $parent = $parent->parent;
147             }
148              
149 351 100 33     1068 my $calling_class = $parent ? ref($parent) : $args{'caller'} || (caller)[0];
150              
151 351         652 my $first_parent = $parent;
152              
153 351   33     1020 my $args = $args{'args'} || $message->args;
154 351   33     1150 my $locale = $args{'locale'} || $message->locale || $self->locale;
155              
156 351         890 my $id = $message->id;
157              
158 351   33     1722 my $variant = $args{'variant'} ||=
159             $self->select_variant_for_message(id => $id,
160             args => $args,
161             locale => $locale);
162              
163 351   50     1133 my $locale_cascade = $self->locale_cascade($locale) ||
164             $self->locale_cascade('default') || [];
165              
166 351         970 foreach my $try_locale ($locale, @$locale_cascade)
167             {
168 376   50     1206 my $variant_cascade =
169             $self->variant_cascade(locale => $try_locale,
170             variant => $variant,
171             message => $message,
172             args => $args) || [];
173              
174 376         844 foreach my $try_variant ($variant, @$variant_cascade)
175             {
176 391         1477 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         810 $parent = $first_parent;
184              
185             # Look for messages in parents
186 391   100     1396 while(!defined $text && $parent)
187             {
188 44 50       352 $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       243 if($parent)
194             {
195 4         25 $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       1456 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   54736 no warnings 'uninitialized';
  44         109  
  44         9669  
215 132 100   132   470 return $_[0] unless(ref $_[0] eq 'CODE');
216 38         108 return $_[0]->();
217             }
218              
219             sub process_placeholders
220             {
221 351     351 0 929 my($self, $text, $args) = @_;
222              
223 351 50       1361 my %args = $args ? %$args : ();
224              
225             # Values will be modified in-place
226 351         912 foreach my $value (values %args)
227             {
228 196 100       622 if(my $ref = ref($value))
229             {
230 68 100       222 if($ref eq 'ARRAY')
231             {
232 28         96 $value = [ map { _evaluate($_) } @$value ];
  92         222  
233             }
234             else
235             {
236 40         126 $value = _evaluate($value);
237             }
238             }
239             }
240              
241 44     44   303 no warnings 'uninitialized';
  44         105  
  44         43403  
242              
243 351         773 for($text)
244             {
245             # Process [@123(...)] and [@foo(...)] placeholders
246 351         1258 s{ ( (?:\\.|[^\[]*)* ) \[ \@ (\d+ | [a-zA-Z]\w* ) (?: \( (.*) \) )? \] }
247 15 100       117 { $1 . join(defined $3 ? $3 : ', ', ref $args{$2} ? @{$args{$2}} : $args{$2}) }gex;
  15 50       115  
248              
249             # Process [123] and [foo] placeholders
250 351         1554 s{ ( (?:\\.|[^\[]*)* ) \[ (\d+ | [a-zA-Z]\w* ) \] }{$1$args{$2}}gx;
251              
252             # Unescape escaped opening square brackets
253 351         911 s/\\\[/[/g;
254             }
255              
256 351         4528 return $text;
257             }
258              
259 395     395 1 1478 sub get_message_name { shift->messages_class->get_message_name(@_) }
260 1     1 1 13 sub get_message_id { shift->messages_class->get_message_id(@_) }
261              
262 0     0 1 0 sub get_error_name { shift->errors_class->get_error_name(@_) }
263 4     4 1 30 sub get_error_id { shift->errors_class->get_error_id(@_) }
264              
265             sub message_for_error_id
266             {
267 70     70 1 261 my($self, %args) = @_;
268              
269 70         207 my $error_id = $args{'error_id'};
270 70   66     241 my $msg_class = $args{'msg_class'} || $self->message_class;
271 70   100     336 my $args = $args{'args'} || [];
272              
273 70         275 my $messages_class = $self->messages_class;
274              
275 70 50       797 if(defined $messages_class->get_message_name($error_id))
    0          
276             {
277 70         1201 return $msg_class->new(id => $error_id, args => $args);
278             }
279             elsif($error_id !~ /^\d+$/)
280             {
281 0         0 croak "Unknown error id: $error_id";
282             }
283              
284 0         0 return $msg_class->new(args => $args);
285             }
286              
287             sub select_variant_for_message
288             {
289 351     351 1 1559 my($self, %args) = @_;
290              
291 351         793 my $args = $args{'args'};
292              
293 351 100       1004 return $args->{'variant'} if($args->{'variant'});
294              
295 345 100       954 if(defined(my $count = $args->{'count'}))
296             {
297 12         69 return $self->select_variant_for_count(%args, count => $count);
298             }
299              
300 333         1413 return DEFAULT_VARIANT;
301             }
302              
303             sub select_variant_for_count
304             {
305 12     12 1 57 my($self, %args) = @_;
306              
307 12   33     42 my $locale = $args{'locale'} || $self->locale;
308 12         33 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             # No default judgements on "few" and "many"
320 12 100       113 return $count == 0 ? 'zero' :
    100          
    100          
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             {
342 376     376 1 1511 my($self, %args) = @_;
343 376   100     2306 return $Variant_Cascade{$args{'variant'}} ||
344             \@None;
345             }
346              
347             sub localized_message_exists
348             {
349 10293     10293 0 21312 my($self, $name, $locale, $variant) = @_;
350              
351 10293         27293 my $msgs = $self->localized_messages_hash;
352              
353 10293   50     97171 $variant ||= DEFAULT_VARIANT;
354              
355 44     44   410 no warnings 'uninitialized';
  44         104  
  44         64515  
356 10293 100 100     40078 if(exists $msgs->{$name} && exists $msgs->{$name}{$locale})
357             {
358 7713 100       21207 if(ref $msgs->{$name}{$locale})
    50          
359             {
360 2572 50       7244 return $msgs->{$name}{$locale}{$variant} ? 1 : 0;
361             }
362             elsif($variant eq DEFAULT_VARIANT)
363             {
364 0         0 return 1;
365             }
366             }
367              
368 7721         20472 return 0;
369             }
370              
371             sub locales_for_message_name
372             {
373 0     0 0 0 my($self, $name) = @_;
374              
375 0         0 my $msgs = $self->localized_messages_hash;
376              
377 0 0       0 return wantarray ? () : [] unless(ref $msgs->{$name});
    0          
378              
379 0         0 return wantarray ? (sort keys %{$msgs->{$name}}) :
380 0 0       0 [ sort keys %{$msgs->{$name}} ];
  0         0  
381             }
382              
383 7     7 0 83 sub add_localized_message_text { shift->set_localized_message_text(@_) }
384              
385             sub set_localized_message_text
386             {
387 102     102 1 586 my($self, %args) = @_;
388              
389 102         259 my $id = $args{'id'};
390 102         235 my $name = $args{'name'};
391 102   66     329 my $locale = $args{'locale'} || $self->locale;
392 102         230 my $text = $args{'text'};
393 102         266 my $variant = $args{'variant'};
394              
395 102 50       296 croak "Missing new localized message text" unless(defined $text);
396              
397 102 50       408 if($name =~ /[^A-Z0-9_]/)
398             {
399 0         0 croak "Message names must be uppercase and may contain only ",
400             "letters, numbers, and underscores";
401             }
402              
403 102 50 33     578 if($id && $name)
    50          
    50          
404             {
405 0 0       0 unless($name eq $self->messages_class->get_message_name($id))
406             {
407 0         0 croak "The message id '$id' does not match the name '$name'";
408             }
409             }
410             elsif(!defined $name)
411             {
412 0 0       0 croak "Missing message id" unless(defined $id);
413 0 0       0 $name = $self->messages_class->get_message_name($id)
414             or croak "No such message id - '$id'";
415             }
416             elsif(!defined $id)
417             {
418 102 50       307 croak "Missing message name" unless(defined $name);
419 102 50       418 $id = $self->messages_class->get_message_id($name)
420             or croak "No such message name - '$name'";
421             }
422              
423 102 100       373 unless(ref $text eq 'HASH')
424             {
425 100         394 $text = { $locale => $text };
426             }
427              
428 102         381 my $msgs = $self->localized_messages_hash;
429              
430 102         1423 while(my($l, $t) = each(%$text))
431             {
432 104 0       338 $Debug && warn qq($self - Adding text $name),
    50          
433             ($variant ? "($variant)" : ''),
434             qq( [$l] - "$t"\n);
435              
436 104 100       797 if($variant)
437             {
438 7 100       30 if(ref $msgs->{$name}{$l})
439             {
440 5         34 $msgs->{$name}{$l}{$variant} = "$t"; # force stringification
441             }
442             else
443             {
444 2         5 my $existing = $msgs->{$name}{$l};
445              
446 2 50       8 if(defined $existing)
447             {
448 0         0 $msgs->{$name}{$l} = {};
449 0         0 $msgs->{$name}{$l}{DEFAULT_VARIANT()} = $existing;
450             }
451              
452 2         13 $msgs->{$name}{$l}{$variant} = "$t"; # force stringification
453             }
454             }
455             else
456             {
457 97 50       383 if(ref ref $msgs->{$name}{$l})
458             {
459 0         0 $msgs->{$name}{$l}{DEFAULT_VARIANT()} = "$t"; # force stringification
460             }
461             else
462             {
463 97         543 $msgs->{$name}{$l} = "$t"; # force stringification
464             }
465             }
466             }
467              
468 102         431 return $id;
469             }
470              
471             sub import_message_ids
472             {
473 2     2 0 28 my($self) = shift;
474              
475 2 50       10 if($Rose::HTML::Object::Exporter::Target_Class)
476             {
477 0         0 $self->messages_class->import(@_);
478             }
479             else
480             {
481 2         8 local $Rose::HTML::Object::Exporter::Target_Class = (caller)[0];
482 2         13 $self->messages_class->import(@_);
483             }
484             }
485              
486             sub import_error_ids
487             {
488 1     1 0 9 my($self) = shift;
489              
490 1 50       3 @_ = (':all') unless(@_);
491              
492 1 50       4 if($Rose::HTML::Object::Exporter::Target_Class)
493             {
494 0         0 $self->errors_class->import(@_);
495             }
496             else
497             {
498 1         4 local $Rose::HTML::Object::Exporter::Target_Class = (caller)[0];
499 1         5 $self->errors_class->import(@_);
500             }
501             }
502              
503             sub add_localized_message
504             {
505 18     18 1 760 my($self, %args) = @_;
506              
507 18   66     132 my $id = $args{'id'} || $self->generate_message_id;
508 18   66     177 my $name = $args{'name'} || croak "Missing name for new localized message";
509 17   33     112 my $locale = $args{'locale'} || $self->locale;
510 17         136 my $text = $args{'text'};
511              
512 17 100       153 croak "Missing new localized message text" unless(defined $text);
513              
514 16 50       83 if($name =~ /[^A-Z0-9_]/)
515             {
516 0         0 croak "Message names must be uppercase and may contain only ",
517             "letters, numbers, and underscores";
518             }
519              
520 16 100       87 unless(ref $text eq 'HASH')
521             {
522 5         17 $text = { $locale => $text };
523             }
524              
525 16         73 my $msgs = $self->localized_messages_hash;
526 16         154 my $msgs_class = $self->messages_class;
527              
528 16         104 my $const = "${msgs_class}::$name";
529              
530 16 100       143 if(defined &$const)
531             {
532 1         238 croak "A constant or subroutine named $name already exists in the class $msgs_class";
533             }
534              
535 15         94 $msgs_class->add_message($name, $id);
536              
537 15         92 while(my($l, $t) = each(%$text))
538             {
539 26 50       94 $Debug && warn qq($self - Adding message $name ($l) = "$t"\n);
540 26         148 $msgs->{$name}{$l} = "$t"; # force stringification
541             }
542              
543 15         75 return $id;
544             }
545              
546 44     44   388 use constant NEW_ID_OFFSET => 100_000;
  44         108  
  44         27899  
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             {
553 16     16 1 44 my($self) = shift;
554              
555 16         89 my $messages_class = $self->messages_class;
556 16         176 my $errors_class = $self->errors_class;
557              
558 16         99 my $new_id = $Last_Generated_Error_Id;
559 16   100     149 $new_id++ while($messages_class->message_id_exists($new_id) ||
560             $errors_class->error_id_exists($new_id));
561              
562 16         249 return $Last_Generated_Message_Id = $new_id;
563             }
564              
565             sub generate_error_id
566             {
567 3     3 1 7 my($self) = shift;
568              
569 3         10 my $errors_class = $self->errors_class;
570 3         27 my $messages_class = $self->messages_class;
571              
572 3         15 my $new_id = $Last_Generated_Error_Id;
573 3   100     19 $new_id++ while($errors_class->error_id_exists($new_id) ||
574             $messages_class->message_id_exists($new_id));
575              
576 3         22 return $Last_Generated_Error_Id = $new_id;
577             }
578              
579             sub add_localized_error
580             {
581 6     6 1 63 my($self, %args) = @_;
582              
583 6   66     28 my $id = $args{'id'} || $self->generate_error_id;
584 6 100       117 my $name = $args{'name'} or croak "Missing localized error name";
585              
586 5         11 my $errors_class = $self->errors_class;
587              
588 5         25 my $const = "${errors_class}::$name";
589              
590 5 100       25 if(defined &$const)
591             {
592 1         105 croak "A constant or subroutine named $name already exists in the class $errors_class";
593             }
594              
595 4         15 $errors_class->add_error($name, $id);
596              
597 4         26 return $id;
598             }
599              
600             sub dump_messages
601             {
602 0     0 0 0 my($self, $code) = @_;
603 0         0 my $msgs = $self->localized_messages_hash;
604 0 0       0 return $code->($msgs) if($code);
605 0         0 require Data::Dumper;
606 0         0 return Data::Dumper::Dumper($msgs);
607             }
608              
609             sub get_localized_message_text
610             {
611 395     395 1 2681 my($self, %args) = @_;
612              
613 395         882 my $id = $args{'id'};
614 395         872 my $name = $args{'name'};
615 395   33     1132 my $locale = $args{'locale'} || $self->locale;
616 395   50     1081 my $variant = $args{'variant'} || DEFAULT_VARIANT;
617 395         770 my $from_class = $args{'from_class'};
618              
619 395   33     957 $from_class ||= (caller)[0];
620              
621 395   33     1765 $name ||= $self->get_message_name($id);
622              
623 395         5593 my $msgs = $self->localized_messages_hash;
624              
625             # Try this twice: before and after loading messages
626 395         3915 foreach my $try (1, 2)
627             {
628 44     44   398 no warnings 'uninitialized';
  44         97  
  44         59520  
629 490 100 100     2376 if(exists $msgs->{$name} && exists $msgs->{$name}{$locale})
630             {
631 383 100 100     1156 if(ref $msgs->{$name}{$locale} && exists $msgs->{$name}{$locale}{$variant})
632             {
633 12         64 return $msgs->{$name}{$locale}{$variant};
634             }
635              
636 371 100       2069 return $msgs->{$name}{$locale} if($variant eq DEFAULT_VARIANT);
637             }
638              
639 139 100       2208 last if($try == 2);
640              
641 95         403 $self->load_localized_message($name, $locale, $variant, $from_class);
642             }
643              
644 44         313 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             {
660 10292     10292 0 23481 my($self, $name, $locale, $variant, $from_class) = @_;
661              
662 10292   33     22818 $from_class ||= $self->messages_class;
663              
664 10292 50       22685 if($self->localized_message_exists($name, $locale, $variant))
665             {
666 0         0 return $self->get_localized_message_text(name => $name,
667             locale => $locale,
668             variant => $variant);
669             }
670              
671 44     44   376 no strict 'refs';
  44         102  
  44         6853  
672 10292         16477 my $fh = \*{"${from_class}::DATA"};
  10292         34960  
673              
674 10292 100       24671 if(fileno($fh))
675             {
676 944         5197 local $/ = "\n";
677              
678 944 100       2629 if($Data_Pos{$from_class})
679             {
680             # Rewind to the start of the __DATA__ section
681 912         9433 seek($fh, $Data_Pos{$from_class}, 0);
682             }
683             else
684             {
685 32         120 $Data_Pos{$from_class} = tell($fh);
686             }
687              
688 944         4080 my $text = $self->load_messages_from_fh(fh => $fh,
689             locales => $locale,
690             variants => $variant,
691             names => $name,
692             force_utf8 => 1);
693 944 100       5510 return $text if(defined $text);
694             }
695              
696 44     44   316 no strict 'refs';
  44         96  
  44         27160  
697              
698 10241         16588 my @classes = @{"${from_class}::ISA"};
  10241         35469  
699 10241         16610 my %seen;
700              
701 10241         22804 while(@classes)
702             {
703 10197         17941 my $class = pop(@classes);
704 10197 50       29119 next if($seen{$class}++);
705             #$Debug && warn "$self SEARCHING $class FOR $name ($locale)\n";
706 10197         26730 my $msg = $self->load_localized_message($name, $locale, $variant, $class);
707 10197 100       23183 return $msg if(defined $msg);
708 10157         16049 push(@classes, grep { !$seen{$_} } @{"${class}::ISA"});
  5054         20070  
  10157         36739  
709             }
710              
711 10201         26874 return undef;
712             }
713              
714             sub auto_load_locales
715             {
716 4     4 0 32 my($self_or_class) = shift;
717              
718 4   33     16 my $class = ref($self_or_class) || $self_or_class;
719              
720 4 100       15 if(@_)
721             {
722 1 50 33     11 my $locales = (@_ == 1 && ref $_[0] eq 'ARRAY') ? [ @{$_[0]} ] : [ @_ ];
  0         0  
723 1         6 return $class->_auto_load_locales($locales);
724             }
725              
726 3         16 my $locales = $class->_auto_load_locales;
727 3 50       108 return wantarray ? @$locales : $locales if(defined $locales);
    100          
728              
729 2 100       12 if(my $locales = $ENV{'RHTMLO_LOCALES'})
730             {
731 1 50       13 $locales = [ split(/\s*,\s*/, $locales) ] unless(ref $locales);
732 1         5 $class->_auto_load_locales($locales);
733 1 50       12 return wantarray ? @$locales : $locales;
734             }
735              
736 1 50       5 return wantarray ? () : [];
737             }
738              
739             sub auto_load_messages
740             {
741 128     128 1 7046 my($self_or_class) = shift;
742              
743 128   33     649 my $class = ref($self_or_class) || $self_or_class;
744              
745 128 50       573 if(@_)
746             {
747 0         0 return $class->_auto_load_messages(@_);
748             }
749              
750 128         1303 my $ret = $class->_auto_load_messages;
751 128 50       4329 return $ret if(defined $ret);
752              
753 128 100 0     1367 if(($ENV{'MOD_PERL'} && (!defined($ENV{'RHTMLO_PRIME_CACHES'}) || $ENV{'RHTMLO_PRIME_CACHES'})) ||
      33        
      66        
754             $ENV{'RHTMLO_PRIME_CACHES'})
755             {
756 1         5 return $class->_auto_load_messages(1);
757             }
758              
759 127         810 return undef;
760             }
761              
762             sub load_all_messages
763             {
764 3     3 1 127 my($class) = shift;
765              
766 3         9 my %args;
767              
768 3 100       13 if(@_ > 1)
769             {
770 1         5 %args = @_;
771             }
772             else
773             {
774 2         7 $args{'from_class'} = $_[0];
775             }
776              
777 3   66     32 my $from_class = $args{'from_class'} || (caller)[0];
778              
779 44     44   407 no strict 'refs';
  44         125  
  44         29074  
780 3         7 my $fh = \*{"${from_class}::DATA"};
  3         17  
781              
782 3 50       16 if(fileno($fh))
783             {
784 3         17 local $/ = "\n";
785              
786 3 100       20 if($Data_Pos{$from_class})
787             {
788             # Rewind to the start of the __DATA__ section
789 2         26 seek($fh, $Data_Pos{$from_class}, 0);
790             }
791             else
792             {
793 1         4 $Data_Pos{$from_class} = tell($fh);
794             }
795              
796 3         14 my $locales = $class->auto_load_locales;
797              
798 3 50       11 $Debug && warn "$class - Loading messages from DATA section of $from_class\n";
799 3         78 $class->load_messages_from_fh(fh => $fh, locales => $locales, force_utf8 => 1);
800             }
801             }
802              
803             sub load_messages_from_file
804             {
805 2     2 1 87 my($self) = shift;
806              
807 2         4 my %args;
808 2 50       8 if(@_ == 1)
    0          
809             {
810 2         8 $args{'file'} = shift;
811             }
812             elsif(@_ > 1)
813             {
814 0 0       0 croak "Odd number of arguments passed to load_messages_from_file()"
815             if(@_ % 2 != 0);
816 0         0 %args = @_;
817             }
818              
819 2 50       9 my $file = delete $args{'file'} or croak "Missing file argument";
820              
821 2 50       224 open($args{'fh'}, $file) or croak "Could no open messages file '$file' - $!";
822 2         18 $self->load_messages_from_fh(%args);
823 2         36 close($args{'fh'});
824             }
825              
826             sub load_messages_from_fh
827             {
828 950     950 0 5252 my($self, %args) = @_;
829              
830 950         3134 my($fh, $locales, $variants, $msg_names) = @args{qw(fh locales variants names)};
831              
832 950 100       5813 binmode($fh, ':utf8') if($args{'force_utf8'});
833              
834 950 100 66     4781 if(ref $locales eq 'ARRAY')
    100          
835             {
836 3 100       12 $locales = @$locales ? { map { $_ => 1} @$locales } : undef;
  3         13  
837             }
838             elsif($locales && !ref $locales)
839             {
840 944         2847 $locales = { $locales => 1 };
841             }
842              
843 950 50 66     4961 if(ref $variants eq 'ARRAY')
    100          
844             {
845 0 0       0 $variants = @$variants ? { map { $_ => 1} @$variants } : undef;
  0         0  
846             }
847             elsif($variants && !ref $variants)
848             {
849 944         2487 $variants = { $variants => 1 };
850             }
851              
852 950         1699 my $msg_re;
853              
854 950 100       2384 if($msg_names)
855             {
856 944 50       1980 if(!ref $msg_names)
    0          
    0          
857             {
858 944         2456 $msg_names = { $msg_names => 1 };
859             }
860             elsif(ref $msg_names eq 'ARRAY')
861             {
862 0         0 $msg_names = { map { $_ => 1 } @$msg_names };
  0         0  
863             }
864             elsif(ref $msg_names eq 'Regexp')
865             {
866 0         0 $msg_re = $msg_names;
867 0         0 $msg_names = undef;
868             }
869             }
870              
871 950         1684 my @text;
872 950         1836 my $in_locale = '';
873 950         1747 my $in_msg = '';
874 950         2101 my $variant = '';
875 950         1684 my $text = '';
876              
877 950         1865 my $pos = tell($fh);;
878              
879 44     44   380 no strict 'refs';
  44         140  
  44         33582  
880              
881 950         1671 local $_;
882              
883 950         17522 while(<$fh>)
884             {
885 32215 100       235543 last if(/$End_Messages/o);
886              
887             #$Debug && warn "PROC: $_";
888              
889 31301 100 33     236326 if(/$End_Message/o && (!$2 || $2 eq $in_msg))
    100 66        
    100          
    100          
    100          
    50          
890             {
891 4 0 33     14 if(!$msg_names || $msg_names->{$in_msg} || ($msg_re && $in_msg =~ /$msg_re/))
      0        
      0        
892             {
893 4         11 for($text)
894             {
895 4         16 s/\A(\s*\n)+//;
896 4         47 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              
905 4         16 $self->set_localized_message_text(name => $in_msg,
906             locale => $in_locale,
907             variant => $variant,
908             text => $text);
909             }
910              
911 4         9 $text = '';
912 4         8 $in_msg = '';
913 4         15 $variant = '';
914             }
915             elsif($in_msg)
916             {
917 4         13 $text .= $_;
918             }
919             elsif(/$Locale_Declaration/o)
920             {
921 3453         15671 $in_locale = $1;
922             }
923             elsif(/$Message_Spec/o)
924             {
925 16701 100 100     103310 if((!$locales || $locales->{$in_locale}) &&
      100        
      100        
      100        
      100        
926             (!$variants || $variants->{$2 || DEFAULT_VARIANT}) &&
927             (!$msg_names || $msg_names->{$1}))
928             {
929 91         281 my $name = $1;
930 91         206 $variant = $2;
931 91         268 my $text = $3;
932              
933 91         256 for($text)
934             {
935 91         265 s/\\n/\n/g;
936 91         300 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              
945 91         497 $self->set_localized_message_text(name => $name,
946             locale => $in_locale,
947             text => $text,
948             variant => $variant);
949 91 100       794 push(@text, $text) if($msg_names);
950             }
951             }
952             elsif(/$Start_Message/o)
953             {
954 4         17 $in_msg = $1;
955 4         18 $variant = $2;
956             }
957             elsif(!/$Comment_Or_Blank/o)
958             {
959 0         0 chomp;
960 0         0 carp "WARNING: Localized message line not understood: $_";
961             }
962             }
963              
964             # Rewind to the starting position
965 950         11210 seek($fh, $pos, 0);
966              
967 950 50       8786 return wantarray ? @text : $text[0];
968 0         0 return;
969             }
970              
971             sub load_messages_from_string
972             {
973 1     1 0 43 my($self) = shift;
974              
975 1 50       7 my %args = @_ == 1 ? (string => shift) : @_;
976              
977 1         867 require IO::String;
978              
979 1         5552 $args{'fh'} = IO::String->new(delete $args{'string'});
980              
981 1         70 return $self->load_messages_from_fh(%args);
982             }
983              
984 44     44   22744 use utf8; # The __END__ section contains UTF-8 text
  44         11384  
  44         317  
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.