1package Rose::HTML::Object::Message::Localizer;
2
3use strict;
4
5use Carp;
6use Clone::PP();
7use Scalar::Util();
8
9use Rose::HTML::Object::Errors();
10use Rose::HTML::Object::Messages();
11
12use base 'Rose::Object';
13
14our $VERSION = '0.606';
15
16our $Debug = 0;
17
18use constant DEFAULT_VARIANT => 'default';
19
20#
21# Object data
22#
23
24use Rose::Object::MakeMethods::Generic
25(
26  '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);
40
41#
42# Class data
43#
44
45use Rose::Class::MakeMethods::Generic
46(
47  inheritable_hash => 'default_locale_cascade',
48);
49
50use Rose::Class::MakeMethods::Generic
51(
52  inheritable_scalar =>
53  [
54    'default_locale',
55    '_auto_load_messages',
56    '_auto_load_locales',
57  ],
58);
59
60__PACKAGE__->default_locale('en');
61__PACKAGE__->default_locale_cascade('default' => [ 'en' ]);
62
63#
64# Class methods
65#
66
67sub default_variant { DEFAULT_VARIANT }
68
69#
70# Object methods
71#
72
73sub init_localized_messages_hash { {} }
74
75sub init_locale_cascade
76{
77  my($self) = shift;
78  my $class = ref($self) || $self;
79  return $class->default_locale_cascade;
80}
81
82sub locale_cascade
83{
84  my($self) = shift;
85
86  my $hash = $self->{'locale_cascade'} ||= ref($self)->init_locale_cascade;
87
88  if(@_)
89  {
90    if(@_ == 1)
91    {
92      return $hash->{$_[0]};
93    }
94    elsif(@_ % 2 == 0)
95    {
96      for(my $i = 0; $i < @_; $i += 2)
97      {
98        $hash->{$_[$i]} = $_[$i + 1];
99      }
100    }
101    else { croak "Odd number of arguments passed to locale_cascade()" }
102  }
103
104  return wantarray ? %$hash : $hash;
105}
106
107sub init_locale
108{
109  my($self) = shift;
110  my $class = ref($self) || $self;
111  return $class->default_locale;
112}
113
114sub init_messages_class { 'Rose::HTML::Object::Messages' }
115sub init_message_class  { 'Rose::HTML::Object::Message::Localized' }
116sub init_errors_class   { 'Rose::HTML::Object::Errors' }
117sub init_error_class    { 'Rose::HTML::Object::Error' }
118
119sub clone { Clone::PP::clone(shift) }
120
121sub parent
122{
123  my($self) = shift;
124  return Scalar::Util::weaken($self->{'parent'} = shift)  if(@_);
125  return $self->{'parent'};
126}
127
128sub localize_message
129{
130  my($self, %args) = @_;
131
132  my $message = $args{'message'};
133
134  return $message  unless($message->can('text') && $message->can('id'));
135  return $message->text  if($message->is_custom);
136
137  my $parent = $message;
138
139  if($parent->can('parent'))
140  {
141    $parent = $parent->parent;
142  }
143
144  if($parent && $parent->isa('Rose::HTML::Object::Error'))
145  {
146    $parent = $parent->parent;
147  }
148
149  my $calling_class = $parent ? ref($parent) : $args{'caller'} || (caller)[0];
150
151  my $first_parent = $parent;
152
153  my $args   = $args{'args'}   || $message->args;
154  my $locale = $args{'locale'} || $message->locale || $self->locale;
155
156  my $id = $message->id;
157
158  my $variant = $args{'variant'} ||=
159    $self->select_variant_for_message(id     => $id,
160                                      args   => $args,
161                                      locale => $locale);
162
163  my $locale_cascade = $self->locale_cascade($locale) ||
164                       $self->locale_cascade('default') || [];
165
166  foreach my $try_locale ($locale, @$locale_cascade)
167  {
168    my $variant_cascade =
169      $self->variant_cascade(locale  => $try_locale,
170                             variant => $variant,
171                             message => $message,
172                             args    => $args) || [];
173
174    foreach my $try_variant ($variant, @$variant_cascade)
175    {
176      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      $parent = $first_parent;
184
185      # Look for messages in parents
186      while(!defined $text && $parent)
187      {
188        $parent = $parent->can('parent_field') ? $parent->parent_field :
189                  $parent->can('parent_form')  ? $parent->parent_form  :
190                  $parent->can('parent')       ? $parent->parent       :
191                  undef;
192
193        if($parent)
194        {
195          $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      return $self->process_placeholders($text, $args)  if(defined $text);
205    }
206  }
207
208  return undef;
209}
210
211# All this to avoid making Scalar::Defer a prerequisite....sigh.
212sub _evaluate
213{
214  no warnings 'uninitialized';
215  return $_[0]  unless(ref $_[0] eq 'CODE');
216  return $_[0]->();
217}
218
219sub process_placeholders
220{
221  my($self, $text, $args) = @_;
222
223  my %args = $args ? %$args : ();
224
225   # Values will be modified in-place
226  foreach my $value (values %args)
227  {
228    if(my $ref = ref($value))
229    {
230      if($ref eq 'ARRAY')
231      {
232        $value = [ map { _evaluate($_) } @$value ];
233      }
234      else
235      {
236        $value = _evaluate($value);
237      }
238    }
239  }
240
241  no warnings 'uninitialized';
242
243  for($text)
244  {
245    # Process [@123(...)] and [@foo(...)] placeholders
246    s{ ( (?:\\.|[^\[]*)* ) \[ \@ (\d+ | [a-zA-Z]\w* ) (?: \( (.*) \) )? \] }
247     { $1 . join(defined $3 ? $3 : ', ', ref $args{$2} ? @{$args{$2}} : $args{$2}) }gex;
248
249    # Process [123] and [foo] placeholders
250    s{ ( (?:\\.|[^\[]*)* ) \[ (\d+ | [a-zA-Z]\w* ) \] }{$1$args{$2}}gx;
251
252    # Unescape escaped opening square brackets
253    s/\\\[/[/g;
254  }
255
256  return $text;
257}
258
259sub get_message_name { shift->messages_class->get_message_name(@_) }
260sub get_message_id   { shift->messages_class->get_message_id(@_) }
261
262sub get_error_name { shift->errors_class->get_error_name(@_) }
263sub get_error_id   { shift->errors_class->get_error_id(@_) }
264
265sub message_for_error_id
266{
267  my($self, %args) = @_;
268
269  my $error_id  = $args{'error_id'};
270  my $msg_class = $args{'msg_class'} || $self->message_class;
271  my $args      = $args{'args'} || [];
272
273  my $messages_class = $self->messages_class;
274
275  if(defined $messages_class->get_message_name($error_id))
276  {
277    return $msg_class->new(id => $error_id, args => $args);
278  }
279  elsif($error_id !~ /^\d+$/)
280  {
281    croak "Unknown error id: $error_id";
282  }
283
284  return $msg_class->new(args => $args);
285}
286
287sub select_variant_for_message
288{
289  my($self, %args) = @_;
290
291  my $args = $args{'args'};
292
293  return $args->{'variant'}  if($args->{'variant'});
294
295  if(defined(my $count = $args->{'count'}))
296  {
297    return $self->select_variant_for_count(%args, count => $count);
298  }
299
300  return DEFAULT_VARIANT;
301}
302
303sub select_variant_for_count
304{
305  my($self, %args) = @_;
306
307  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  # No default judgements on "few" and "many"
320  return $count == 0 ? 'zero' :
321         $count == 1 ? 'one'  :
322         $count == 2 ? 'two'  :
323         'plural';
324}
325
326my %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
338my @None;
339
340sub variant_cascade
341{
342  my($self, %args) = @_;
343  return $Variant_Cascade{$args{'variant'}} ||
344         \@None;
345}
346
347sub localized_message_exists
348{
349  my($self, $name, $locale, $variant) = @_;
350
351  my $msgs = $self->localized_messages_hash;
352
353  $variant ||= DEFAULT_VARIANT;
354
355  no warnings 'uninitialized';
356  if(exists $msgs->{$name} && exists $msgs->{$name}{$locale})
357  {
358    if(ref $msgs->{$name}{$locale})
359    {
360      return $msgs->{$name}{$locale}{$variant} ? 1 : 0;
361    }
362    elsif($variant eq DEFAULT_VARIANT)
363    {
364      return 1;
365    }
366  }
367
368  return 0;
369}
370
371sub locales_for_message_name
372{
373  my($self, $name) = @_;
374
375  my $msgs = $self->localized_messages_hash;
376
377  return wantarray ? () : []  unless(ref $msgs->{$name});
378
379  return wantarray ? (sort keys %{$msgs->{$name}}) :
380                     [ sort keys %{$msgs->{$name}} ];
381}
382
383sub add_localized_message_text { shift->set_localized_message_text(@_) }
384
385sub set_localized_message_text
386{
387  my($self, %args) = @_;
388
389  my $id      = $args{'id'};
390  my $name    = $args{'name'};
391  my $locale  = $args{'locale'} || $self->locale;
392  my $text    = $args{'text'};
393  my $variant = $args{'variant'};
394
395  croak "Missing new localized message text"  unless(defined $text);
396
397  if($name =~ /[^A-Z0-9_]/)
398  {
399    croak "Message names must be uppercase and may contain only ",
400          "letters, numbers, and underscores";
401  }
402
403  if($id && $name)
404  {
405    unless($name eq $self->messages_class->get_message_name($id))
406    {
407      croak "The message id '$id' does not match the name '$name'";
408    }
409  }
410  elsif(!defined $name)
411  {
412    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  {
418    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
423  unless(ref $text eq 'HASH')
424  {
425    $text = { $locale => $text };
426  }
427
428  my $msgs = $self->localized_messages_hash;
429
430  while(my($l, $t) = each(%$text))
431  {
432    $Debug && warn qq($self - Adding text $name),
433                   ($variant ? "($variant)" : ''),
434                   qq( [$l] - "$t"\n);
435
436    if($variant)
437    {
438      if(ref $msgs->{$name}{$l})
439      {
440        $msgs->{$name}{$l}{$variant} = "$t"; # force stringification
441      }
442      else
443      {
444        my $existing = $msgs->{$name}{$l};
445
446        if(defined $existing)
447        {
448          $msgs->{$name}{$l} = {};
449          $msgs->{$name}{$l}{DEFAULT_VARIANT()} = $existing;
450        }
451
452        $msgs->{$name}{$l}{$variant} = "$t"; # force stringification
453      }
454    }
455    else
456    {
457      if(ref ref $msgs->{$name}{$l})
458      {
459        $msgs->{$name}{$l}{DEFAULT_VARIANT()} = "$t"; # force stringification
460      }
461      else
462      {
463        $msgs->{$name}{$l} = "$t"; # force stringification
464      }
465    }
466  }
467
468  return $id;
469}
470
471sub import_message_ids
472{
473  my($self) = shift;
474
475  if($Rose::HTML::Object::Exporter::Target_Class)
476  {
477    $self->messages_class->import(@_);
478  }
479  else
480  {
481    local $Rose::HTML::Object::Exporter::Target_Class = (caller)[0];
482    $self->messages_class->import(@_);
483  }
484}
485
486sub import_error_ids
487{
488  my($self) = shift;
489
490  @_ = (':all')  unless(@_);
491
492  if($Rose::HTML::Object::Exporter::Target_Class)
493  {
494    $self->errors_class->import(@_);
495  }
496  else
497  {
498    local $Rose::HTML::Object::Exporter::Target_Class = (caller)[0];
499    $self->errors_class->import(@_);
500  }
501}
502
503sub add_localized_message
504{
505  my($self, %args) = @_;
506
507  my $id     = $args{'id'} || $self->generate_message_id;
508  my $name   = $args{'name'} || croak "Missing name for new localized message";
509  my $locale = $args{'locale'} || $self->locale;
510  my $text   = $args{'text'};
511
512  croak "Missing new localized message text"  unless(defined $text);
513
514  if($name =~ /[^A-Z0-9_]/)
515  {
516    croak "Message names must be uppercase and may contain only ",
517          "letters, numbers, and underscores";
518  }
519
520  unless(ref $text eq 'HASH')
521  {
522    $text = { $locale => $text };
523  }
524
525  my $msgs = $self->localized_messages_hash;
526  my $msgs_class = $self->messages_class;
527
528  my $const = "${msgs_class}::$name";
529
530  if(defined &$const)
531  {
532    croak "A constant or subroutine named $name already exists in the class $msgs_class";
533  }
534
535  $msgs_class->add_message($name, $id);
536
537  while(my($l, $t) = each(%$text))
538  {
539    $Debug && warn qq($self - Adding message $name ($l) = "$t"\n);
540    $msgs->{$name}{$l} = "$t"; # force stringification
541  }
542
543  return $id;
544}
545
546use constant NEW_ID_OFFSET => 100_000;
547
548our $Last_Generated_Message_Id = NEW_ID_OFFSET;
549our $Last_Generated_Error_Id   = NEW_ID_OFFSET;
550
551sub generate_message_id
552{
553  my($self) = shift;
554
555  my $messages_class = $self->messages_class;
556  my $errors_class = $self->errors_class;
557
558  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
562  return $Last_Generated_Message_Id = $new_id;
563}
564
565sub generate_error_id
566{
567  my($self) = shift;
568
569  my $errors_class = $self->errors_class;
570  my $messages_class = $self->messages_class;
571
572  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
576  return $Last_Generated_Error_Id = $new_id;
577}
578
579sub add_localized_error
580{
581  my($self, %args) = @_;
582
583  my $id   = $args{'id'} || $self->generate_error_id;
584  my $name = $args{'name'} or croak "Missing localized error name";
585
586  my $errors_class = $self->errors_class;
587
588  my $const = "${errors_class}::$name";
589
590  if(defined &$const)
591  {
592    croak "A constant or subroutine named $name already exists in the class $errors_class";
593  }
594
595  $errors_class->add_error($name, $id);
596
597  return $id;
598}
599
600sub dump_messages
601{
602  my($self, $code) = @_;
603  my $msgs = $self->localized_messages_hash;
604  return $code->($msgs)  if($code);
605  require Data::Dumper;
606  return Data::Dumper::Dumper($msgs);
607}
608
609sub get_localized_message_text
610{
611  my($self, %args) = @_;
612
613  my $id         = $args{'id'};
614  my $name       = $args{'name'};
615  my $locale     = $args{'locale'} || $self->locale;
616  my $variant    = $args{'variant'} || DEFAULT_VARIANT;
617  my $from_class = $args{'from_class'};
618
619  $from_class ||= (caller)[0];
620
621  $name ||= $self->get_message_name($id);
622
623  my $msgs = $self->localized_messages_hash;
624
625  # Try this twice: before and after loading messages
626  foreach my $try (1, 2)
627  {
628    no warnings 'uninitialized';
629    if(exists $msgs->{$name} && exists $msgs->{$name}{$locale})
630    {
631      if(ref $msgs->{$name}{$locale} && exists $msgs->{$name}{$locale}{$variant})
632      {
633        return $msgs->{$name}{$locale}{$variant};
634      }
635
636      return $msgs->{$name}{$locale}  if($variant eq DEFAULT_VARIANT);
637    }
638
639    last  if($try == 2);
640
641    $self->load_localized_message($name, $locale, $variant, $from_class);
642  }
643
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*\))?
649my $Locale_Declaration = qr{^\s* \[% \s* LOCALE \s* (\S+) \s* %\] \s* (?: \#.*)?$}x;
650my $Start_Message = qr{^\s* \[% \s* START \s+ ([A-Z0-9_]+)(?:\(\s*([-\w]+)\s*\))? \s* %\] \s* (?: \#.*)?$}x;
651my $End_Message = qr{^\s* \[% \s* END \s+ ([A-Z0-9_]+)(?:\(\s*([-\w]+)\s*\))?? \s* %\] \s* (?: \#.*)?$}x;
652my $Message_Spec = qr{^ \s* ([A-Z0-9_]+)(?:\(\s*([-\w]+)\s*\))? \s* = \s* "((?:[^"\\]+|\\.)*)" \s* (?: \#.*)? $}x;
653my $Comment_Or_Blank = qr{^ \s* \# | ^ \s* $}x;
654my $End_Messages = qr{^=\w|^\s*__END__};
655
656my %Data_Pos;
657
658sub load_localized_message
659{
660  my($self, $name, $locale, $variant, $from_class) = @_;
661
662  $from_class ||= $self->messages_class;
663
664  if($self->localized_message_exists($name, $locale, $variant))
665  {
666    return $self->get_localized_message_text(name   => $name,
667                                            locale  => $locale,
668                                            variant => $variant);
669  }
670
671  no strict 'refs';
672  my $fh = \*{"${from_class}::DATA"};
673
674  if(fileno($fh))
675  {
676    local $/ = "\n";
677
678    if($Data_Pos{$from_class})
679    {
680      # Rewind to the start of the __DATA__ section
681      seek($fh, $Data_Pos{$from_class}, 0);
682    }
683    else
684    {
685      $Data_Pos{$from_class} = tell($fh);
686    }
687
688    my $text = $self->load_messages_from_fh(fh         => $fh,
689                                            locales    => $locale,
690                                            variants   => $variant,
691                                            names      => $name,
692                                            force_utf8 => 1);
693    return $text  if(defined $text);
694  }
695
696  no strict 'refs';
697
698  my @classes = @{"${from_class}::ISA"};
699  my %seen;
700
701  while(@classes)
702  {
703    my $class = pop(@classes);
704    next  if($seen{$class}++);
705    #$Debug && warn "$self SEARCHING $class FOR $name ($locale)\n";
706    my $msg = $self->load_localized_message($name, $locale, $variant, $class);
707    return $msg  if(defined $msg);
708    push(@classes, grep { !$seen{$_} } @{"${class}::ISA"});
709  }
710
711  return undef;
712}
713
714sub auto_load_locales
715{
716  my($self_or_class) = shift;
717
718  my $class = ref($self_or_class) || $self_or_class;
719
720  if(@_)
721  {
722    my $locales = (@_ == 1 && ref $_[0] eq 'ARRAY') ? [ @{$_[0]} ] : [ @_ ];
723    return $class->_auto_load_locales($locales);
724  }
725
726  my $locales = $class->_auto_load_locales;
727  return wantarray ? @$locales : $locales  if(defined $locales);
728
729  if(my $locales = $ENV{'RHTMLO_LOCALES'})
730  {
731    $locales = [ split(/\s*,\s*/, $locales) ]  unless(ref $locales);
732    $class->_auto_load_locales($locales);
733    return wantarray ? @$locales : $locales;
734  }
735
736  return wantarray ? () : [];
737}
738
739sub auto_load_messages
740{
741  my($self_or_class) = shift;
742
743  my $class = ref($self_or_class) || $self_or_class;
744
745  if(@_)
746  {
747    return $class->_auto_load_messages(@_);
748  }
749
750  my $ret = $class->_auto_load_messages;
751  return $ret  if(defined $ret);
752
753  if(($ENV{'MOD_PERL'} && (!defined($ENV{'RHTMLO_PRIME_CACHES'}) || $ENV{'RHTMLO_PRIME_CACHES'})) ||
754     $ENV{'RHTMLO_PRIME_CACHES'})
755  {
756    return $class->_auto_load_messages(1);
757  }
758
759  return undef;
760}
761
762sub load_all_messages
763{
764  my($class) = shift;
765
766  my %args;
767
768  if(@_ > 1)
769  {
770    %args = @_;
771  }
772  else
773  {
774    $args{'from_class'} = $_[0];
775  }
776
777  my $from_class = $args{'from_class'} || (caller)[0];
778
779  no strict 'refs';
780  my $fh = \*{"${from_class}::DATA"};
781
782  if(fileno($fh))
783  {
784    local $/ = "\n";
785
786    if($Data_Pos{$from_class})
787    {
788      # Rewind to the start of the __DATA__ section
789      seek($fh, $Data_Pos{$from_class}, 0);
790    }
791    else
792    {
793      $Data_Pos{$from_class} = tell($fh);
794    }
795
796    my $locales = $class->auto_load_locales;
797
798    $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
803sub load_messages_from_file
804{
805  my($self) = shift;
806
807  my %args;
808  if(@_ == 1)
809  {
810    $args{'file'} = shift;
811  }
812  elsif(@_ > 1)
813  {
814    croak "Odd number of arguments passed to load_messages_from_file()"
815      if(@_ % 2 != 0);
816    %args = @_;
817  }
818
819  my $file = delete $args{'file'} or croak "Missing file argument";
820
821  open($args{'fh'}, $file) or croak "Could no open messages file '$file' - $!";
822  $self->load_messages_from_fh(%args);
823  close($args{'fh'});
824}
825
826sub load_messages_from_fh
827{
828  my($self, %args) = @_;
829
830  my($fh, $locales, $variants, $msg_names) = @args{qw(fh locales variants names)};
831
832  binmode($fh, ':utf8')  if($args{'force_utf8'});
833
834  if(ref $locales eq 'ARRAY')
835  {
836    $locales = @$locales ? { map { $_ => 1} @$locales } : undef;
837  }
838  elsif($locales && !ref $locales)
839  {
840    $locales = { $locales => 1 };
841  }
842
843  if(ref $variants eq 'ARRAY')
844  {
845    $variants = @$variants ? { map { $_ => 1} @$variants } : undef;
846  }
847  elsif($variants && !ref $variants)
848  {
849    $variants = { $variants => 1 };
850  }
851
852  my $msg_re;
853
854  if($msg_names)
855  {
856    if(!ref $msg_names)
857    {
858      $msg_names = { $msg_names => 1 };
859    }
860    elsif(ref $msg_names eq 'ARRAY')
861    {
862      $msg_names = { map { $_ => 1 } @$msg_names };
863    }
864    elsif(ref $msg_names eq 'Regexp')
865    {
866      $msg_re = $msg_names;
867      $msg_names = undef;
868    }
869  }
870
871  my @text;
872  my $in_locale = '';
873  my $in_msg    = '';
874  my $variant   = '';
875  my $text      = '';
876
877  my $pos = tell($fh);;
878
879  no strict 'refs';
880
881  local $_;
882
883  while(<$fh>)
884  {
885    last  if(/$End_Messages/o);
886
887    #$Debug && warn "PROC: $_";
888
889    if(/$End_Message/o && (!$2 || $2 eq $in_msg))
890    {
891      if(!$msg_names || $msg_names->{$in_msg} || ($msg_re && $in_msg =~ /$msg_re/))
892      {
893        for($text)
894        {
895          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
905        $self->set_localized_message_text(name    => $in_msg,
906                                          locale  => $in_locale,
907                                          variant => $variant,
908                                          text    => $text);
909      }
910
911      $text    = '';
912      $in_msg  = '';
913      $variant = '';
914    }
915    elsif($in_msg)
916    {
917      $text .= $_;
918    }
919    elsif(/$Locale_Declaration/o)
920    {
921      $in_locale = $1;
922    }
923    elsif(/$Message_Spec/o)
924    {
925      if((!$locales || $locales->{$in_locale}) &&
926         (!$variants || $variants->{$2 || DEFAULT_VARIANT}) &&
927         (!$msg_names || $msg_names->{$1}))
928      {
929        my $name = $1;
930        $variant = $2;
931        my $text = $3;
932
933        for($text)
934        {
935          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
945        $self->set_localized_message_text(name    => $name,
946                                          locale  => $in_locale,
947                                          text    => $text,
948                                          variant => $variant);
949        push(@text, $text)  if($msg_names);
950      }
951    }
952    elsif(/$Start_Message/o)
953    {
954      $in_msg  = $1;
955      $variant = $2;
956    }
957    elsif(!/$Comment_Or_Blank/o)
958    {
959      chomp;
960      carp "WARNING: Localized message line not understood: $_";
961    }
962  }
963
964  # Rewind to the starting position
965  seek($fh, $pos, 0);
966
967  return wantarray ? @text : $text[0];
968  return;
969}
970
971sub load_messages_from_string
972{
973  my($self) = shift;
974
975  my %args = @_ == 1 ? (string => shift) : @_;
976
977  require IO::String;
978
979  $args{'fh'} = IO::String->new(delete $args{'string'});
980
981  return $self->load_messages_from_fh(%args);
982}
983
984use utf8; # The __END__ section contains UTF-8 text
985
9861;
987
988__END__
989
990=encoding utf-8
991
992=head1 NAME
993
994Rose::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
1035L<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
1037In 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
1039This 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
1043L<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
1045The 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
1071Note how detecting the exact error did not require regex-matching against error message text or anything similarly unmaintainable.
1072
1073When 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
1077Broadly 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
1079The 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
1108The 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
1110Localized text is provided in double-quoted strings to the right of symbolic L<messages|Rose::HTML::Object::Messages> constant names.
1111
1112Placeholders 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
1116In such a case, the values are joined with ", " to form the text that replaces the placeholder.
1117
1118Embedded 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
1122The resulting text:
1123
1124    Here[]:
1125    A backslash \ and some "embedded" double quotes
1126
1127There'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
1134Leading and trailing spaces and newlines are removed from text provided in the multi-line format.
1135
1136Blank lines and any lines beginning with a C<#> character are skipped.
1137
1138=head3 VARIANTS
1139
1140Any 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
1144is equivalent to this:
1145
1146    SOME_MESSAGE = "..."
1147
1148Before 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
1150With 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
1152Requesting 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
1156Aside 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
1158If 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
1160This 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
1162L<http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html>
1163
1164with 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
1166The 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
1168But 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
1170The 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
1174Here'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
1191Now 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
1239I 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
1241And 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
1245The 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
1247The 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
1249You 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
1253Localization 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
1262Localized 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
1270Get 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
1274Get 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
1278Get 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
1282That is, if message text is not available in the desired locale, C<en> text will be returned instead (assuming it exists).
1283
1284This 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
1288Load 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
1294The 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
1306Constructs a new L<Rose::HTML::Object::Message::Localizer> object based on PARAMS, where PARAMS are
1307name/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
1317Add a new localized error message.  PARAMS are name/value pairs.  Valid PARAMS are:
1318
1319=over 4
1320
1321=item B<id ID>
1322
1323An 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
1327An 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
1333Add a new localized message.  PARAMS are name/value pairs.  Valid PARAMS are:
1334
1335=over 4
1336
1337=item B<id ID>
1338
1339An 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
1343A 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
1349Returns 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
1353Get 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
1357Get 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
1361Get 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
1365Get 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
1369Get 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
1373Returns 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
1377Returns 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
1381This 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
1385This 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
1389Returns localized message text based on PARAMS name/value pairs.  Valid PARAMS are:
1390
1391=over 4
1392
1393=item B<id ID>
1394
1395An 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
1399The 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
1403The 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
1407The 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
1413This 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
1417This 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
1421Load 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
1423If 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
1429The path to the file.  This parameter is required.
1430
1431=item B<locales [ LOCALE | ARRAYREF ]>
1432
1433A 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
1437Only 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
1443Get 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
1447Get 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
1449This 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
1453Localize 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
1459A 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
1463The locale.  If omitted, the C<message>'s L<locale|Rose::HTML::Object::Message::Localized/locale> is used.
1464
1465=item B<message MESSAGE>
1466
1467The L<Rose::HTML::Object::Message>-derived message object.  This parameter is required.
1468
1469=item B<variant VARIANT>
1470
1471The 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
1475This 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
1479Given 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
1485The integer error id.  This parameter is required.
1486
1487=item B<args HASHREF>
1488
1489A 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
1495Get or set a weakened reference to the localizer's parent object.
1496
1497=item B<select_variant_for_count PARAMS>
1498
1499Select 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
1505A reference to a hash of L<message arguments|Rose::HTML::Object::Message::Localized/args>.
1506
1507=item B<count INTEGER>
1508
1509The count for which to select a variant.  This parameter is required.
1510
1511=item B<locale LOCALE>
1512
1513The L<locale|/LOCALES> of the localized message text.  Defaults to the localizer's L<locale()|/locale> if omitted.
1514
1515=back
1516
1517The 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
1526See 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
1530Select 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
1536A reference to a hash of L<message arguments|Rose::HTML::Object::Message::Localized/args>.
1537
1538=item B<id MESSAGEID>
1539
1540The L<message id|Rose::HTML::Object::Messages>.
1541
1542=item B<locale LOCALE>
1543
1544The L<locale|/LOCALES> of the localized message text.  Defaults to the localizer's L<locale()|/locale> if omitted.
1545
1546=back
1547
1548If 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
1550If C<args> contains a C<variant> parameter, then the value of that parameter is returned.
1551
1552Otherwise, the L<default_variant|/default_variant> is returned.
1553
1554=item B<set_localized_message_text PARAMS>
1555
1556Set the localized text for a message.  Valid PARAMS name/value pairs are:
1557
1558=over 4
1559
1560=item B<id ID>
1561
1562An 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
1566The 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
1570The L<locale|/LOCALES> of the localized message text.  Defaults to the localizer's L<locale|/locale>.
1571
1572=item B<text TEXT>
1573
1574The localized message text.
1575
1576=item B<variant VARIANT>
1577
1578The 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
1584Return 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
1590A reference to a hash of L<message arguments|Rose::HTML::Object::Message::Localized/args>.
1591
1592=item B<locale LOCALE>
1593
1594The L<locale|/LOCALES> of the desired localized message text.
1595
1596=item B<message MESSAGE>
1597
1598The L<Rose::HTML::Object::Message>-derived message object.
1599
1600=item B<variant VARIANT>
1601
1602The originally requested message L<variant|/"VARIANTS">.
1603
1604=back
1605
1606The 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
1617The array references returned should be treated as read-only.
1618
1619=back
1620
1621=head1 AUTHOR
1622
1623John C. Siracusa (siracusa@gmail.com)
1624
1625=head1 LICENSE
1626
1627Copyright (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.
1628