File Coverage

blib/lib/Locale/MakePhrase/RuleManager.pm
Criterion Covered Total %
statement 142 154 92.2
branch 68 116 58.6
condition 15 27 55.5
subroutine 16 16 100.0
pod 5 5 100.0
total 246 318 77.3


line stmt bran cond sub pod time code
1             package Locale::MakePhrase::RuleManager;
2             our $VERSION = 0.4;
3             our $DEBUG = 0;
4              
5             =head1 NAME
6              
7             Locale::MakePhrase::RuleManager - Language rule sort and evaluation
8             object.
9              
10             =head1 SYNOPSIS
11              
12             The L module uses this plugin module, to implement
13             the evaluation and sorting phases of text selection. It explains the
14             rule expression syntax and evaluation procedure.
15              
16             It sorts the language rules into a suitable order so that we can
17             figure out which rule to select, ie. the aim is to sort the rules into
18             an order so that we can select the first rule.
19              
20             It evaluates the program arguments against the expressions in the rule.
21              
22             =head1 PHRASE SYNTAX
23              
24             To allow an argument to be placed within the middle of a string, we
25             use square brackets as the notation, plus an underscore, then the
26             argument number, as in:
27              
28             "Please select [_1] files"
29              
30             where I<[_1]> refers to the first program variable supplied as an
31             argument with the text to be translated.
32              
33             To display square brackets within the text string, you will need to
34             escape the square bracket by using the B<~> (tilde) character, as in:
35              
36             "This is ~[ bracketed text ~]"
37              
38             this will print:
39              
40             This is [ bracketed text ]
41              
42             Of course, if you need to display the B<~> character, you will need to
43             use two of them, as in:
44              
45             "Tilde needs escaping as in ~~"
46              
47             which ends up printing:
48              
49             Tilde needs escaping as in ~
50              
51             =head1 LINGUISTIC RULES
52              
53             We have coined the term I as a means to describe the
54             technique which decides which piece of text is displayed, for a given
55             input text phrase and any/all program arguments.
56              
57             To understand why we need to generate linguistic rules, consider the
58             'singular vs. plural' example shown in the L
59             section.
60              
61             In this example, we needed four different text strings, for the
62             trivial case of what to display for a given program value.
63              
64             For other examples, the URL's mentioned in that section describe why
65             there is a need for applying rules on a per-language basis (they also
66             describe why most current language translation systems fail).
67              
68             =head2 What is a linguistic rule?
69              
70             A linguistic rule is the evaluation of the context of a phrase by
71             using program arguments, for a given program string. The arguments
72             are evaluated left-to-right and top-to-bottom. The first rule to
73             succeed has its corresponding translated text applied in-place of the
74             input text.
75              
76             Note that if a program string takes no arguments, the rule becomes
77             rather simplistic (in that no arguments need to be evaluated).
78              
79             Rules can be tested in a number of ways. The 'Operators' and
80             'Functions' sections list the available rule expression conjigates
81             available for use within each rule expression.
82              
83             Previously we mentioned that the language translation system used
84             syntax with the form B<[_1]>. You will notice that we use an
85             underscore in the placeholder. This may appear to be meaningless, but
86             as we will see, we use this rule property to help understand how rules
87             are evaluated.
88              
89             =head2 Numeric evaluation
90              
91             Let's show an example of a simple expression:
92              
93             _1 == 0
94              
95             The use of the underscore signifies that this value is to be
96             classified as an argument number and is not to be treated literally.
97             This expression says, 'Does the first argument have a value equal to
98             zero?'
99              
100             [Note that we use double-equals; the double-equals operator will use
101             a numeric context in the equality test.]
102              
103             =head2 String evaluation
104              
105             Since an argument can also be a string, we could define an expression
106             to be:
107              
108             _1 eq "some text"
109              
110             Notice that we use a different operator depending on whether the
111             argument is numeric or a string. This is because we need to be able
112             to figure out what context the argument needs to be evaluated in.
113              
114             [In this case we use 'eq' as the text context equality operator.]
115              
116             =head2 Alternate argument representation
117              
118             In some cases we need to be able to specify the translated string,
119             based on an alternate representation of the argument. This is handled
120             by using a function. For example, you may use the term 'houses',
121             which is the main keyword within your application.
122              
123             To handle alternations of the word 'houses' (such as 'house') we can
124             define an expression of:
125              
126             left(_1,5) eq 'house'
127              
128             However, in some cases we will use the terms 'apartments' or 'flats'.
129             In these cases, we only care if the value is in the plural or singular
130             case:
131              
132             right(_1,1) eq "s"
133              
134             Thus, we are provided with a set of functions which allow some
135             manipulation of the argument value, prior to evaluation.
136              
137             =head2 Multiple arguments
138              
139             In many cases, more than one argument is supplied (as well as the text
140             to translate) to L. In those cases, an expression
141             can be created which tests each argument, as in:
142              
143             _1 == 0 && _2 != 1
144              
145             As we can see here, by using B<&&>, we combine multiple expression
146             evaluations, into a single rule expression. In this case the
147             expression is effectively saying "if argument one is equal to zero AND
148             argument two is not equal to one".
149              
150             We support an unlimited number of arguments within the expression
151             evaluation capability.
152              
153             =head2 Multiple rule expressions
154              
155             Consider the following exresssions:
156              
157             _1 > 0 produces the output "Lots of files"
158             _1 == 0 produces "No files"
159             _1 == 1 produces "One file"
160              
161             Each expression is a valid, but if we evaluate this set of expressions
162             in the wrong order, we will never be able to produce the text "One
163             file" as the B<_1 E 0> expression would evaluate to true, before
164             we try to evaluate the B<_1 == 1> expression.
165              
166             To counter this problem, whenever we define a rule expression
167             (including when there is no rule expression as would be the case when
168             no arguments are supplied), we must also define a rule priority (where
169             a larger number gives higher priority).
170              
171             Knowing this, let's re-consider the previous set of expressions, this
172             time adding a suitable priority of evaluation for each expression:
173              
174             expression: priority:
175             _1 > 1 1
176             _1 == 0 2
177             _1 == 1 2
178              
179             Now that we have a rule priority, we can see that the B<_1 == 0>
180             expression and the B<_1 == 1> expression will get evaluated before the
181             B<_1 E 1> expression.
182              
183             You will notice that two rules have the same priority (i.e. we can
184             have any number of rules having the same priority); in this case, the
185             rules are evaluated in a non-deterministic (first found, first
186             evaluated) manner. Thus it is important to make sure that a given
187             rule expression, has a valid rule priority, for the rule set.
188              
189             =head2 Rule Syntax
190              
191             Now that we know what a linguistic rule is, we need to explain some
192             minor but important points.
193              
194             Each symbol in a rule expression needs to be separated with a space,
195             i.e. this works:
196              
197             _1 > 4
198             left(_2,1) eq "f"
199              
200             this doesn't:
201              
202             _1>4
203             left(_2,1)eq"f"
204              
205             Whenever we are using a string operator, we must enquote the value
206             that we are testing, i.e. this works:
207              
208             _1 eq "fred"
209              
210             this doesn't:
211              
212             _1 eq fred
213              
214             We support single and double quote characters, including mixed quoting
215             (for simplistic cases), i.e. these work:
216              
217             _1 eq "some text"
218             _1 eq 'some text'
219             _1 eq "someone's dog"
220             _1 eq '"john spoke"'
221              
222             this doesn't (i.e. there is no quote 'escape' capability):
223              
224             _1 eq "\"something\""
225              
226             Note that expressions are not unary, as in (this checks if the
227             first argument has any length):
228              
229             length(_1)
230              
231             rather, they should look like:
232              
233             length(_1) > 0
234              
235             =head1 APPLYING RULES TO LANGUAGES
236              
237             =over 8
238              
239             =item CAVEAT:
240              
241             The following description of rule evaluation is correct at the time of
242             writing. However, as this module evolves, we may alter the
243             implementation as we get feedback. If you have used this module and
244             found that the rule evaluation order is not what you expect, please
245             contact the maintainer.
246              
247             =back
248              
249             So far we have discussed the concept that, a translation exists for a
250             language/dialect combination. However, the application may not be
251             translated into the specific language requested by the user. In
252             these cases, L tries to use fallback languages as
253             the source language for this translation request. This allows
254             languages derived from other base languages (eg Spanish and Mexican
255             share common words) and dialect specific variations of languages (such
256             as variations of English), to use the parent language as a source for
257             possible translations.
258              
259             Thus whenever a phrase cannot be translated directly into the requested
260             localisation, L will use a fallback mechanisn for
261             the input phrase.
262              
263             Also, to support variations in output text which can exist in
264             locale-specific translations, non-expression rules should be evaluated
265             after rules which have an expression.
266              
267             The implementation of which rule to select, has been abstracted into a
268             seperate module so that you can implement your own process of which
269             rule is selected based on the available rules. The default
270             implementation is defined in L. It
271             contains a description of the current implementation.
272              
273             =head2 Overview of steps to rule evaluation
274              
275             =over 3
276              
277             =item 1.
278              
279             L generates a list of rules which are applicable for
280             required languages (plus any fallback languages).
281              
282             =item 2.
283              
284             The rules are sorted by the L module.
285              
286             =item 3.
287              
288             Each expression from the sorted rules are evaluated. If the rule
289             succeeds, the corresponding text is returned. If not, the next rule
290             is evaluated.
291              
292             =item 4.
293              
294             If finally no match is found, the input string is used as the output
295             string.
296              
297             =item 5.
298              
299             Any arguments are then applied to the output string.
300              
301             =back
302              
303             =head2 Example rule definitions
304              
305             Shown below are examples of various rules; some rules have no
306             expressions and/or arguments; all rules must have at least a priority
307             of zero or more.
308              
309             =over 2
310              
311             =item Rule 1:
312              
313             Language: en_US
314             Input text: Please select some colours.
315             Expression: (none)
316             Priority: 0
317             Output text: Please select some colors.
318              
319             =item Rule 2:
320              
321             Language: en
322             Input text: Please select some colours.
323             Expression: (none)
324             Priority: 0
325             Output text: Please select colours.
326              
327             =item Rule 3:
328              
329             Language: en_AU
330             Input text: Please select [_1] colours.
331             Expression: (none)
332             Priority: 0
333             Output text: Please select [_1] colours.
334              
335             =item Rule 4:
336              
337             Language: en
338             Input text: Please select [_1] colours.
339             Expression: _1 > 0
340             Priority: 0
341             Output text: Select [_1] colours.
342              
343             =back
344              
345             =over 2
346              
347             =item An example:
348              
349             Given that the preferred language is 'en_US', if you compare rule 1 vs
350             rule 2, the linguistic rule evaluation mechanism will be applied to
351             rule 1 before being applied to rule 2, as it has a higher language-order.
352              
353             =item A further example:
354              
355             Compare rule 3 vs rule 4. Given that there is no expression associated
356             with rule 3, but that the 'en' version does have an expression, rule 4
357             will be evaluated (and found to be true in some cases) before example
358             3 is evaluated.
359              
360             =back
361              
362             These examples show that it is important to consider the interactions
363             of the linguistic rules, as they are applied to the current localisation.
364              
365             =head1 APPLYING ARGUMENTS TO TEXT
366              
367             With any text translation system, there comes a time when it is
368             necessary to apply the values of the arguments 'in situ', replacing the
369             square-bracket argument number, with the corresponding argument value,
370             so that the output will say something useful. This happens after all
371             rules have been applied (if there were any), and after the output text
372             string has been chosen.
373              
374             For example:
375              
376             Input text: "Selected [_2] files, [_1] directories"
377             Arguments: 3 21
378              
379             Apply rules...
380              
381             Rule text: "Selected [_2] files, [_1] directories"
382             Output text: "Selected 21 files, 3 directories"
383              
384             =head1 OPERATORS
385              
386             This is a list of all operators:
387              
388             Operator Context Meaning Example
389             ----------------------------------------------------------------------
390             == Numeric Equal to _1 == 4
391             != Numeric Not equal to _1 != 2
392             > Numeric Greater than _2 > 1
393             < Numeric Less than _1 < 7
394             >= Numeric Less than or equal to _4 >= 21
395             <= Numeric Greater than or equal to _3 <= 12
396             eq String Equal to _1 eq "some text"
397             ne String Not equal to _2 ne "something else"
398              
399             =head1 FUNCTIONS
400              
401             This is a list of available functions:
402              
403             Function Context Meaning Example
404             ----------------------------------------------------------------------
405             defined(x) - Is the argument defined/not-null, defined(_1)
406             returns 0 or 1
407             length(x) - Length of value of the argument, length(_1)
408             returns an integer >= 0
409             abs(n) Number Numerical absolute of argument abs(_3)
410             lc(s) String Lowercase version lc(_1)
411             uc(s) String Uppercase version uc(_2)
412             left(s,n) String LHS of argument from start left(_3,4)
413             right(s,n) String RHS of argument from end right(_1,2)
414             substr(s,n) String RHS of argument from start substr(_2,7)
415             substr(s,n,l) String Sub-part of argument from 'n', substr(_2,7,4)
416             up to 'l' characters
417              
418             =head1 API
419              
420             The following functions are used by the L class.
421             By sub-classing this module, then overloading these functions,
422             L can use yor custom RuleManager module.
423              
424             =cut
425              
426 9     9   2230 use strict;
  9         18  
  9         381  
427 9     9   44 use warnings;
  9         17  
  9         235  
428 9     9   978 use utf8;
  9         25  
  9         53  
429 9     9   236 use base qw();
  9         32  
  9         179  
430 9     9   53 use Data::Dumper;
  9         14  
  9         535  
431 9     9   57 use Locale::MakePhrase::Utils qw(is_number left right alltrim die_from_caller);
  9         17  
  9         2922  
432             local $Data::Dumper::Indent = 1 if $DEBUG;
433              
434 9     9   53 use constant STR_INVALID_TRANSLATED => "";
  9         15  
  9         810  
435              
436             # Available datatypes
437 9     9   54 use constant UNKNOWN => -1;
  9         14  
  9         450  
438 9     9   45 use constant UNSPECIFIED => 0;
  9         16  
  9         430  
439 9     9   45 use constant NUMBER => 1;
  9         16  
  9         410  
440 9     9   43 use constant STRING => 2;
  9         21  
  9         31710  
441              
442             # Available operators
443             our %OPERATORS = (
444             '==' => NUMBER,
445             '!=' => NUMBER,
446             '<' => NUMBER,
447             '>' => NUMBER,
448             '<=' => NUMBER,
449             '>=' => NUMBER,
450             'eq' => STRING,
451             'ne' => STRING,
452             );
453              
454             # Available functions
455             our %FUNCTIONS = (
456             'defined' => [ UNSPECIFIED, [], sub { defined($_[0]); } ],
457             'length' => [ UNSPECIFIED, [], sub { length($_[0]); } ],
458             'abs' => [ NUMBER, [], sub { abs($_[0]); } ],
459             'lc' => [ STRING, [], sub { lc($_[0]); } ],
460             'uc' => [ STRING, [], sub { uc($_[0]); } ],
461             'left' => [ STRING, [1], sub { left($_[0],$_[1]); } ],
462             'right' => [ STRING, [1], sub { right($_[0],$_[1]); } ],
463             'substr' => [ STRING, [1,2], sub {
464             return substr($_[0], $_[1]) if @_ == 2;
465             return substr($_[0], $_[1], $_[2]);
466             } ],
467             );
468              
469             # Predefined regular expression patterns
470             my $ops_re; { my $tmp = join ('|', keys %OPERATORS); $ops_re = qr/$tmp/; }
471             my $ltr_re = qr/^_(\d+)\s+($ops_re)\s+(.*)/;
472             my $rtl_re = qr/(.*)\s+($ops_re)\s+_(\d+)$/;
473             my $func_ltr_re = qr/^([a-zA-Z0-9_]+)\(_(\d+)([^)]*)\)\s+($ops_re)\s+(.*)$/;
474             my $func_rtl_re = qr/^(.*)\s+($ops_re)\s+([a-zA-Z0-9_]+)\(_(\d+)([^)]*)\)$/;
475              
476              
477             #--------------------------------------------------------------------------
478              
479             =head2 new()
480              
481             Construct a new instance of L object;
482             arguments are passed to the init() method.
483              
484             =cut
485              
486             # Constructor
487             sub new {
488 8     8 1 213 my $proto = shift;
489 8   33     52 my $class = ref($proto) || $proto;
490 8         34 my $self = bless {}, $class;
491 8         34 return $self->init(@_);
492             }
493              
494             #--------------------------------------------------------------------------
495              
496             =head2 $self init([..])
497              
498             Allow sub-class a chance to control construction of the object. You
499             must return a reference to $self, to 'allow' the construction to
500             complete (should you decide to derive from it).
501              
502             =cut
503              
504 8     8 1 58 sub init { shift }
505              
506             #--------------------------------------------------------------------------
507              
508             =head2 boolean evaluate($expression,@arguments)
509              
510             This is the expression evaluation engine. It takes an expression as
511             described above (for example B<_1 == 4 && _2 eq 'fred'>). It then
512             takes any program arguments, applying them in-place of the B<_X>
513             place holders. Finally returning true / false, based on the result
514             of the evaluation of the expression.
515              
516             =cut
517              
518             sub evaluate {
519 17     17 1 22 my $self = shift;
520 17 50       38 die("Missing rule expression?!") unless @_;
521 17         61 my $expression = alltrim(shift);
522 17 50       45 print STDERR "Evaluating rule: $expression\n" if $DEBUG > 1;
523 17 50       58 print STDERR "Arguments: ". Dumper(\@_) if $DEBUG > 5;
524 17         23 my $evaluation = 0;
525 17         22 my @expressions;
526 17         21 my $arg_count = scalar(@_);
527              
528             # Break apart expression into subexpressions, so that it can be validated
529 17         22 my @subexpressions;
530 17         71 foreach my $chunk (split('\s+&&\s+',$expression)) {
531 17         44 my ($arg,$val,$op,$text,$quote,$func,$func_args) = ("","","","","","","");
532 17         31 my ($val_type,$op_type,$text_type,$func_type) = (UNKNOWN,UNKNOWN,UNKNOWN,UNKNOWN);
533            
534             # Break apart subexpression
535 17 100       350 if ($chunk =~ $ltr_re) {
    100          
    50          
    0          
536 7         27 ($arg,$op,$text) = ($1,$2,$3);
537             } elsif ($chunk =~ $rtl_re) {
538 2         8 ($text,$op,$arg) = ($1,$2,$3);
539             } elsif ($chunk =~ $func_ltr_re) {
540 8         36 ($func,$arg,$func_args,$op,$text) = ($1,$2,$3,$4,$5);
541             } elsif ($chunk =~ $func_rtl_re) {
542 0         0 ($text,$op,$func,$arg,$func_args) = ($1,$2,$3,$4,$5);
543             } else {
544 0         0 die_from_caller("Invalid subexpression ($chunk) found in expression:",$expression);
545             }
546              
547             # Grab properties for this subexpression - test for conformity
548 17 50       46 die_from_caller("Missing argument ?!") unless $arg;
549 17 50       33 die_from_caller("Missing operator ?!") unless $op;
550 17 50       57 die_from_caller("Missing text value ?!") unless length $text;
551 17 50       49 die_from_caller("Unknown operator: $op") unless exists $OPERATORS{$op};
552 17 50       48 die_from_caller("Incorrect number of arguments in expression (trying to use $arg arguments when $arg_count arguments supplied):",$expression) if ($arg > $arg_count);
553 17 100       56 if ($text =~ /^(['"])(.*)(["'])$/) {
554 6 50       26 $quote = $1 if ($1 eq $3);
555 6 50       19 $text = defined $2 ? $2 : "";
556             }
557 17         32 $val = $_[$arg-1];
558 17         33 $op_type = $OPERATORS{$op};
559 17 50       41 $val_type = not defined $val ? UNSPECIFIED : is_number($val) ? NUMBER : STRING;
    100          
560 17 100       60 $text_type = is_number($text) ? NUMBER : STRING;
561 17 50 66     66 die_from_caller("Missing quote-marks for text value: $text") if ($op_type == STRING and not length $quote);
562 17 50 66     78 die_from_caller("Mis-matched text-value/operator types\n- text-value: $text \n- operator: $op") if ($op_type == NUMBER and $text_type != NUMBER);
563              
564 17 0       42 print STDERR "Rule properties:\n- argument: $arg\n- function: $func\n- operator: $op\n- text: $text\n- quote: $quote\n- value: ", defined $val ? $val : "", "\n" if $DEBUG > 1;
    50          
565              
566             # build expression
567 17 100       32 if ($func) {
568 8 50       26 die_from_caller("Unknown function: $func") unless exists $FUNCTIONS{$func};
569 8         21 $func_type = $FUNCTIONS{$func}->[0];
570 8 50 66     34 die_from_caller("Mis-matched function/operator types\n- function: $func\n- operator: $op") if ($func_type != UNSPECIFIED and $func_type != $op_type);
571 8 50 66     31 die_from_caller("Invalid use of undefined argument (argument number: $arg) when used with function: $func") if ($val_type == UNSPECIFIED and $func_type != UNSPECIFIED);
572 8         17 my $required = $FUNCTIONS{$func}->[1];
573 8         14 my $sub = $FUNCTIONS{$func}->[2];
574 8 100       18 if (@$required) {
575 6 50       15 die_from_caller("Incorrect number of arguments for function: $func (need: ". join(',',@$required) ." - none provided)") unless length $func_args;
576 6         17 $func_args = alltrim($func_args);
577 6         46 my @func_args = split(/\s*,\s*/,$func_args);
578 6 50       17 shift @func_args if @func_args > 1;
579 6         10 my $found = 0;
580 6         13 foreach my $required_arg_count (@$required) {
581 10 100       30 $found++ if @func_args == $required_arg_count;
582             }
583 6 50       15 die_from_caller("Incorrect number of arguments for function: $func (need: ". join(',',@$required) ." - provided: ".scalar(@func_args).")") unless $found;
584 6         16 $val = &$sub($val,@func_args);
585             } else {
586 2         6 $val = &$sub($val);
587             }
588 8 50       20 $val = 0 unless defined $val;
589 8         11 $val_type = $op_type;
590 8 0       21 print STDERR "Function result: ", defined $val ? $val : "", "\n" if $DEBUG > 5;
    50          
591             } else {
592 9 50       23 die_from_caller("Invalid use of undefined argument (argument number: $arg) expression: $expression") unless (defined $val);
593             }
594              
595 17 50       43 print STDERR "- op_type $op_type\n- val_type $val_type\n- text_type $text_type\n" if $DEBUG > 3;
596            
597 17 50 66     132 die_from_caller("Mis-matched argument/operator types\n- argument: $val\n- operator: $op") if (($op_type == STRING and $val_type != STRING) or ($op_type == NUMBER and $val_type != NUMBER));
      66        
      33        
598 17         136 push @subexpressions, "$quote$val$quote $op $quote$text$quote";
599             }
600 17 50       55 die("Failed building expression") unless @subexpressions;
601 17         39 my $parsed_expression = join(' && ',@subexpressions);
602            
603             # Evaluate expression - needs to return some sort or true or false value.
604             # Note that under Perl, this is as simple as an 'eval' :-)
605 17         995 my $expr_result = eval $parsed_expression;
606 17 100       59 $expr_result = $expr_result ? 1 : 0;
607 17 50       38 print STDERR "Expression to evaluate: $parsed_expression result: $expr_result\n" if $DEBUG;
608 17         66 return $expr_result;
609             }
610              
611             #--------------------------------------------------------------------------
612              
613             =head2 \@rules sort(\@rule_objects,\@languages)
614              
615             The guts of the sorter; by subclassing this module, you can implement
616             your own sorting routine.
617              
618             This module implements the following rules for deciding the sorted
619             order of the rules. The aim is to return a list which can be
620             evaluated in-order.
621              
622             =over 3
623              
624             =item 1.
625              
626             Rules are sorted histest to lowest priority, for the primary language,
627             for rules which have expressions.
628              
629             =item 2.
630              
631             The next available fallback language is chosen as the language to use;
632             step 1 is repeated.
633              
634             =item 3.
635              
636             This process continues until no further fallback languages are available.
637              
638             =item 4.
639              
640             The non-expression rules are then evaluated according to the preferred
641             language.
642              
643             =item 5.
644              
645             If that fails, the fallback languages are tried. This continues for
646             each fallback language.
647              
648             =back
649              
650             =cut
651              
652             sub sort {
653 11     11 1 16 my ($self,$rule_objs,$languages) = @_;
654 11         15 my @new_order;
655             my @non_ruled;
656 11         22 foreach my $language (@$languages) {
657 22         34 my @r;
658             my @nr;
659 22         32 foreach my $r_obj (@$rule_objs) {
660 54 100       135 next unless $r_obj->language eq $language;
661 27 100       110 if ($r_obj->expression) {
662 16         32 push @r, $r_obj;
663             } else {
664 11         29 push @nr, $r_obj;
665             }
666             }
667 22         50 @r = sort { $b->priority <=> $a->priority } @r;
  5         20  
668 22         34 push @new_order, @r;
669 22         30 @nr = sort { $b->priority <=> $a->priority } @nr;
  0         0  
670 22         55 push @non_ruled, @nr;
671             }
672 11         26 push @new_order, @non_ruled;
673 11         40 return \@new_order;
674             }
675              
676             #--------------------------------------------------------------------------
677              
678             =head2 $string apply_arguments($makephrase,$translation,@arguments)
679              
680             This applies any/all arguments, to the outgoing text phrase; if the
681             argument is text, it (optionally) undergoes the translation process;
682             if the argument is numeric, it is formatted by the L
683             C method.
684              
685             =cut
686              
687             sub apply_arguments {
688 26     26 1 55 my ($self,$makephrase,$translation,@args) = @_;
689 26 50       61 print "HERE:".Dumper(@args) if $DEBUG;
690 26         43 my $arg_count = scalar(@args);
691 26         38 my $output = "";
692 26         34 my $in_group = 0;
693 26         135 WHILE_LOOP: while($translation =~ /\G(
694             [^\~\[\]]+ # non-~[] stuff
695             |
696             ~[\[\]\~] # ~[, ~], ~~
697             |
698             \[ # [ presumably opening a group
699             |
700             \] # ] presumably closing a group
701             |
702             .* # any other characters
703             |
704             $
705             )/xgs ){
706 85 50       220 my $chunk = defined $1 ? $1 : "";
707 85 100       359 if ($chunk eq '[') {
    100          
    100          
    50          
708 10 50       26 die_from_caller("Found recursive beginning square-bracket:",$translation) if $in_group;
709 10         36 $in_group++;
710             } elsif ($chunk eq ']') {
711 10         14 $in_group--;
712 10 50       55 die_from_caller("Found recursive ending square-bracket:",$translation) if $in_group;
713             } elsif ($in_group) {
714             # inside square bracket group
715 10         37 $chunk = alltrim($chunk);
716 10 50       41 if ($chunk =~ /^_/) {
717 10         38 my ($idx,$options) = split(/\s*,\s*/,$chunk,2);
718 10 50       26 $options = { split(/\s*(?:(?:=>)|(?:,))\s*/,$options) } if $options;
719 10         33 $idx =~ s/^_//;
720 10         23 $idx = int($idx)-1;
721 10 50       24 if ($idx >= $arg_count) {
722 0 0       0 die_from_caller("Incorrect number of arguments used in translation - supplied $arg_count, tried to use at least $idx.") if ($makephrase->{die_on_bad_translation});
723 0         0 $output .= STR_INVALID_TRANSLATED;
724 0         0 last WHILE_LOOP;
725             } else {
726 10         16 my $val = $args[$idx];
727 10 50 33     66 if (defined $val and length $val) {
728 10 100       35 if (is_number($val)) {
    50          
729 8         38 $output .= $makephrase->format_number($val,$options);
730             } elsif ($makephrase->{translate_arguments}) {
731 2         10 $output .= $makephrase->translate($val);
732             } else {
733 0         0 $output .= $val;
734             }
735             }
736             }
737             } else {
738 0 0       0 if ($makephrase->{die_on_bad_translation}) {
739 0         0 die_from_caller("Invalid translated string: $translation");
740             }
741 0         0 $output .= STR_INVALID_TRANSLATED;
742 0         0 last WHILE_LOOP;
743             }
744             } elsif (substr($chunk,0,1) eq '~') {
745 0         0 $output .= substr($chunk,1);
746             } else {
747 55         275 $output .= $chunk;
748             }
749             }
750              
751 26         114 return $output;
752             }
753              
754             1;
755             __END__