File Coverage

blib/lib/Mojolicious/Plugin/Localize.pm
Criterion Covered Total %
statement 178 190 93.6
branch 92 106 86.7
condition 50 62 80.6
subroutine 17 18 94.4
pod 1 1 100.0
total 338 377 89.6


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Localize;
2 12     12   8886652 use Mojo::Base 'Mojolicious::Plugin';
  12         31  
  12         92  
3 12     12   3541 use Mojo::Util qw/decode/;
  12         23  
  12         847  
4 12     12   117 use Mojo::File qw/path/;
  12         21  
  12         608  
5 12     12   5602 use Mojolicious::Plugin::Config;
  12         13369  
  12         101  
6 12     12   542 use File::Spec::Functions 'file_name_is_absolute';
  12         25  
  12         692  
7 12     12   6573 use List::MoreUtils 'uniq';
  12         171312  
  12         72  
8              
9             # TODO:
10             # Wrap http://search.cpan.org/~reneeb/Mojolicious-Plugin-I18NUtils-0.05/lib/Mojolicious/Plugin/I18NUtils.pm
11             # TODO:
12             # do not backtrack on upper case dictionary keys
13             # TODO:
14             # Support prefixes in dictionary
15             # TODO:
16             # Support locale sub in dictionary
17             # TODO:
18             # 'd' is probably better than 'loc'
19             # 'd' for dictionary lookup
20             # TODO:
21             # use Hash::Merge or Hash::Merge::Small
22             # TODO:
23             # Use Mojo::Template directly
24             # TODO:
25             # deal with:
26             # <%= numsep $g_count %> <%= quant $g_count, 'guest', 'guests' %> online.'
27             # TODO:
28             # Deal with bidirectional text
29              
30 12   50 12   11798 use constant DEBUG => $ENV{MOJO_LOCALIZE_DEBUG} || 0;
  12         23  
  12         42080  
31             our $VERSION = '0.23';
32              
33             has 'log';
34              
35             # Warning: This only works for default EP templates
36             our $TEMPLATE_INDICATOR = qr/(?:^\s*\%)|<\%/m;
37              
38             # Register plugin
39             sub register {
40 49     49 1 330630 my ($self, $mojo, $param) = @_;
41              
42 49         188 my (@dict, @resources);
43 49 100       332 @dict = ($param->{dict}) if $param->{dict}; # Hashes
44 49 100       234 @resources = @{$param->{resources}} if $param->{resources}; # File names
  2         11  
45              
46 49         279 $self->log($mojo->log);
47              
48             # Not yet initialized
49 49 100       1547 unless ($mojo->renderer->helpers->{loc}) {
50              
51             # Load parameter from config file
52 13 100       289 if (my $c_param = $mojo->config('Localize')) {
53              
54             # Prefer the configuration dictionary
55 1 50       17 push @dict, $c_param->{dict} if $c_param->{dict};
56              
57             # Prefer the configuration override parameter
58 1 50       3 $param->{override} = $c_param->{override} if $c_param->{override};
59              
60             # Add configuration resources
61 1 50       3 if ($c_param->{resources}) {
62 1         2 unshift @resources, @{$c_param->{resources}};
  1         2  
63             };
64             };
65              
66             # Load default helper
67 13         262 $mojo->plugin('Localize::Quantify');
68 13         1713 $mojo->plugin('Localize::Locale');
69              
70             # Add 'generate dictionary' command
71 13         5056 push @{$mojo->commands->namespaces}, __PACKAGE__ . '::Command';
  13         133  
72              
73             # Lookup a dictionary key and return the value
74             $mojo->helper(
75             loc => sub {
76 124     124   78549 my $c = shift;
77              
78             # Nothing to look up
79 124 100 100     797 return '' unless scalar @_ && $_[0];
80              
81 120         562 my $key = [split('_', shift)];
82              
83 120         222 if (DEBUG) {
84             _debug($c->app, '[LOOKUP] Search for "' . join('_', @$key) . '"');
85             };
86              
87             # If a default entry is given, get it
88 120 100 100     386 my $default_entry = shift if @_ && @_ % 2 != 0;
89              
90             # Store all other values in the stash
91 120         292 my %stash = @_;
92              
93             # Return dictionary entry or default entry
94 120   100     532 return _lookup($c, \%stash, $c->stash('localize.dict'), $key, 0) ||
      100        
95             $default_entry // '';
96             }
97 13         1134 );
98              
99              
100             # Return the dictionary reference
101             $mojo->helper(
102             'localize.dictionary' => sub {
103             # Return the complete dictionary in case no parameter is defined
104             # This is not documented and may change in further versions
105 20     20   11230 return $_[0]->stash('localize.dict');
106             }
107 13         962 );
108              
109             # Return the prefered path
110             $mojo->helper(
111             'localize.preference' => sub {
112 4     4   388 my $c = shift;
113              
114 4         13 my $stash = $c->stash;
115 4 50       28 return $stash->{'localize.preference'} if $stash->{'localize.preference'};
116              
117 4   100     15 my $key = [split('_', shift // '')];
118 4         9 my $dict = $c->stash('localize.dict');
119              
120 4         23 if (DEBUG) {
121             _debug($c->app, '[PREF] Look for prefered key for "' . join('_', @$key) . '"');
122             };
123              
124             # If a default entry is given, get it
125 4         6 my $default_entry = shift;
126              
127             # Return dictionary key - so pass the "find_pref" parameter
128 4   33     6 $stash->{'localize.preference'} = _lookup($c, {}, $c->stash('localize.dict'), $key, 0, 1) ||
      50        
129             $default_entry // '';
130 4         20 return $stash->{'localize.preference'};
131             }
132 13         3483 );
133              
134 13         3832 $mojo->defaults('localize.dict' => {});
135             };
136              
137             # Merge dictionary resources
138 49 100       884 if (@resources) {
139              
140             # Create config loader
141 3         24 my $config_loader = Mojolicious::Plugin::Config->new;
142 3         30 my $home = $mojo->home;
143              
144             # Load files
145 3         26 foreach my $file (uniq @resources) {
146              
147 4 100       25 $file = $home->rel_file($file) unless file_name_is_absolute $file;
148              
149 4         126 if (DEBUG) {
150             _debug($mojo, "Load dictionary $file");
151             };
152              
153 4 50       16 if (-e $file) {
154 4 50       174 if (my $dict = $config_loader->load($file, undef, $mojo)) {
155 4         2288 unshift @dict, [$dict, $file];
156 4         22 _debug($mojo, qq!Successfully loaded dictionary "$file"!);
157 4         79 next;
158             };
159             };
160 0         0 $mojo->log->warn(qq!Unable to load dictionary file "$file"!);
161             };
162             };
163              
164 49         218 my $dict_global = $mojo->defaults('localize.dict');
165              
166             # Merge dictionary hashes
167 49         605 foreach (@dict) {
168 49   66     278 my $is_array = ref $_ && ref $_ eq 'ARRAY';
169              
170 49         105 if (DEBUG) {
171             _debug(
172             $mojo,
173             '[MERGE] Start merging' .
174             ($is_array ? (' of ' . $_->[1]) : '')
175             );
176             };
177              
178             # Merge to global dictionary
179 49 100       326 $self->_merge($dict_global, $is_array ? $_->[0] : $_, $param->{override});
180             };
181             };
182              
183              
184             # Unflatten short notation
185             sub _unflatten {
186 12     12   29 my ($key, $dict) = @_;
187 12         27 my $k = $$key;
188 12         24 my $g_hash = $dict->{$k};
189              
190             # Check for preferred key
191 12 100       51 if (substr($k, -1, 1) eq '_') {
192 1         5 $g_hash = { _ => $g_hash };
193 1         4 chop $k;
194             };
195              
196             # Build verbose tree
197 12         122 $g_hash = { $1 => $g_hash } while $k =~ s/_([^_]+)$//;
198              
199             # Set root key
200 12         31 $$key = $k;
201 12         35 $dict->{$k} = $g_hash;
202             };
203              
204              
205             # Store value as string or code reference
206             sub _store {
207 126     126   262 my $value = $_[0];
208              
209             # Is template - store as reference
210 126 100 100     1174 return $value if ref $value || $value =~ $TEMPLATE_INDICATOR;
211 113         496 return \$value;
212             };
213              
214              
215             # Merge dictionaries
216             sub _merge {
217 170     170   419 my ($self, $dict_global, $dict, $override) = @_;
218              
219             # Iterate over all keys
220 170         480 foreach my $k (keys %$dict) {
221              
222             # This is a short notation key
223 299 100       791 if (index($k, '_') > 0) {
    100          
224 12         21 _debug($self, qq![MERGE] Unflatten "$k"!) if DEBUG;
225              
226             # Unflatten short notation
227 12         45 _unflatten(\$k, $dict);
228             }
229              
230             # Set preferred key
231             elsif ($k eq '_') {
232              
233             # If override or not set yet, set the new preferred key
234 47 100 100     229 if ($override || !defined $dict_global->{_}) {
235              
236 46         74 _debug($self, qq![MERGE] Override "_"!) if DEBUG;
237 46         126 $dict_global->{_} = $dict->{_};
238             };
239              
240 47         201 next;
241             };
242              
243             # This is a default key
244 252 100       522 if (index($k, '-') == 0) {
245 35         60 my $standalone = 0;
246              
247 35         72 _debug($self, qq![MERGE] Try to set default key with "$k"!) if DEBUG;
248              
249             # This is a prefixed default key
250 35 100       91 if (length($k) > 1) {
251 32         81 $k = substr($k, 1);
252 32         122 $dict->{$k} = delete $dict->{"-$k"};
253             }
254              
255             # This is a standalone default key
256             else {
257 3         14 $k = $dict->{'-'};
258 3         6 $standalone = 1;
259             };
260              
261             # If override or not set yet, set the new default key
262 35 100 100     158 if ($override || !defined $dict_global->{'-'}) {
263              
264 34         48 _debug($self, qq![MERGE] Override default key with "$k"!) if DEBUG;
265 34         96 $dict_global->{'-'} = $k;
266             };
267              
268 35 100       115 next if $standalone;
269             };
270              
271             # Insert key - if it not yet exists
272 249 100 100     662 if (!$dict_global->{$k}) {
    100          
    100          
273              
274             # Merge the tree
275 220 100       417 if (ref $dict->{$k} eq 'HASH') {
276 97         365 $self->_merge($dict_global->{$k} = {}, $dict->{$k}, $override);
277             }
278              
279             # Store the plain value
280             else {
281 123         303 $dict_global->{$k} = _store($dict->{$k});
282             };
283             }
284              
285             # Merge key, when both are hashes
286             elsif (ref($dict_global->{$k}) eq ref($dict->{$k}) && ref($dict_global->{$k}) eq 'HASH') {
287 24         81 $self->_merge($dict_global->{$k}, $dict->{$k}, $override);
288             }
289              
290             # Override global and store the plain value
291             elsif ($override) {
292 3         21 $dict_global->{$k} = _store($dict->{$k});
293             };
294             };
295             };
296              
297             sub _mark {
298 0     0   0 my ($keys, $level) = @_;
299 0         0 my @x = ();
300 0         0 for (my $i = 0; $i <= $#$keys; $i++) {
301 0 0       0 if ($i == $level) {
302 0         0 push @x, '[' . $keys->[$i] . ']';
303             }
304             else {
305 0         0 push @x, $keys->[$i];
306             };
307             };
308              
309 0         0 return join('_',@x);
310             };
311              
312             # Lookup dictionary entry recursively
313             sub _lookup {
314 351     351   2156 my ($c, $stash, $dict, $key, $level, $find_pref) = @_;
315             # $c is the controller object
316             # $stash contains a hash reference of stash values
317             # $dict contains the dictionary at the current level
318             # $key is the key array passed to the resolver
319             # $level is the current position in the key
320             # $find_pref is a boolean value indicating that no value is looked up
321              
322             # Get the current input element to consume
323 351         530 my @keys;
324 351 100       880 if (my $primary = $key->[$level]) {
325 298         628 @keys = ($primary);
326              
327 298         382 if (DEBUG) {
328             _debug($c->app, qq![LOOKUP] There is a primary key "$primary" at input level [$level]!);
329             _debug($c->app, qq![LOOKUP] at "! . _mark($key, $level) . '"');
330             };
331             };
332              
333             # No primary key given
334              
335             # Check all possibilities
336 351         487 my $pos = 0;
337 351         566 my $lazy = 0;
338              
339              
340             # Iterate over all possible key fragments
341 351         484 while () {
342              
343             # No more keys
344 482 100       914 if (!$keys[$pos]) {
345              
346 150         192 if (DEBUG) {
347             _debug(
348             $c->app,
349             "[LOOKUP] There is no more key at position $pos on input level [$level]"
350             );
351             };
352              
353 150 50 66     318 if ($lazy && $find_pref && $level >= $#{$key}) {
  0   33     0  
354 0         0 return $keys[$pos-1];
355             };
356              
357             # Stop processing
358 150 100       324 return if $lazy;
359              
360             # There is a stop value defined and no primary exists
361 141 100 100     415 push @keys, '.' if $dict->{'.'} && !$keys[0];
362              
363             # Lazy load further keys
364             # Add preferred keys
365 141 100       280 if ($dict->{'_'}) {
366 85         224 my @matches = _get_pref_keys($c, $dict->{'_'}, $stash);
367 85 100       204 if ($matches[0]) {
368 75         110 if (DEBUG) {
369             _debug(
370             $c->app,
371             qq![LOOKUP] But there are preferred keys "@matches"!
372             );
373             };
374 75         196 push @keys, @matches;
375             };
376             };
377              
378             # Add default key
379 141 100       362 if ($dict->{'-'}) {
380 74         142 my $match = $dict->{'-'};
381 74         103 if (DEBUG) {
382             _debug($c->app, qq![LOOKUP] But there is a default key "$match"!);
383             };
384 74 50       287 push @keys, $match if $match;
385             };
386              
387 141 100       484 return unless $keys[$pos];
388              
389             # There may be items set multiple times
390 95         657 @keys = uniq @keys;
391              
392 95         205 _debug($c->app, qq![LOOKUP] Check non-manual keys "@keys"!) if DEBUG;
393              
394 95         153 $lazy = 1;
395             };
396              
397             # Key has a match
398 427 100       1103 if (my $match = $dict->{$keys[$pos]}) {
399              
400             # Debug information
401 343         513 if (DEBUG) {
402             _debug(
403             $c->app,
404             qq![LOOKUP] Found entry for "$keys[$pos]" on input level [$level]!
405             );
406             };
407              
408             # The match is final
409 343 100 100     2459 if ((!ref($match) || ref($match) eq 'SCALAR' || ref($match) eq 'CODE') && !$find_pref) {
    100 66        
      66        
      100        
410              
411             # Everything is cosumed - fine
412 112 50       169 if ($level >= $#{$key}) {
  112         293  
413              
414             # Value is scalar
415 112 100       299 if (ref $match eq 'SCALAR') {
    100          
416 83         145 if (DEBUG) {
417             _debug(
418             $c->app,
419             qq![LOOKUP] Found scalar value "$$match"!
420             );
421             };
422 83         312 return $$match;
423             }
424              
425             # Value is a subroutine
426             elsif (ref $match eq 'CODE') {
427 2         9 my $value = $match->($c, %$stash);
428 2         16 if (DEBUG) {
429             _debug(
430             $c->app,
431             qq![LOOKUP] Found subroutine value as "$value"!
432             );
433             };
434 2         20 return $value;
435             };
436              
437             # Value is a template
438 27         140 my $value = $c->render_to_string(inline => $match, %$stash);
439 27 50       21472 chomp($value) unless delete $stash->{no_trim};
440 27         2364 if (DEBUG) {
441             _debug(
442             $c->app,
443             qq![LOOKUP] Found template value as "$value"!
444             );
445             };
446 27         133 return $value;
447             };
448              
449             # Check another path
450             }
451              
452             # Get the relevant key if everything is consumed
453 6         17 elsif (ref($match) && $find_pref && $level > $#{$key}) {
454              
455 4         5 if (DEBUG) {
456             _debug(
457             $c->app,
458             '[PREF] Found key "' . $keys[$pos] . '"'
459             );
460             };
461              
462 4         14 return $keys[$pos];
463             }
464              
465             # No final match found - go on
466             else {
467              
468 227         332 my $level_up = $level;
469              
470             # If the primary key was consumed or not given, level up
471 227 100 100     596 if (!$pos || !$key->[$level]) {
472 173         255 $level_up++;
473 173         260 if (DEBUG) {
474             _debug($c->app, "[LOOKUP] Forward to input level [$level_up]");
475             };
476             };
477              
478             # Call lookup recursively
479 227         590 my $found = _lookup(
480             $c, $stash, $match, $key, $level_up, $find_pref
481             );
482              
483             # Found something
484 227 100       1520 return $found if $found;
485             };
486             };
487              
488             # Get next key
489 131         215 $pos++;
490 131         185 if (DEBUG) {
491             _debug($c->app, "[LOOKUP] Forward to next key at position $pos");
492             };
493             };
494             };
495              
496              
497             # Debug messages
498             sub _debug {
499 4     4   26 my ($app, $msg) = @_;
500              
501             # If the value is 2 - debug to stderr
502 4         7 if (DEBUG == 2) {
503             print STDERR "$msg\n";
504             }
505              
506             # Otherwise debug to log
507             else {
508 4         19 $app->log->debug($msg);
509             }
510             };
511              
512              
513             # Return preferred keys
514             sub _get_pref_keys {
515 85     85   175 my ($c, $index, $stash) = @_;
516              
517 85 50       191 return unless $index;
518              
519             # Preferred key is a template
520 85 100 33     325 unless (ref $index) {
    100          
521              
522 2         16 my $key = $c->render_to_string(inline => $index, %$stash);
523 2 50       3587 chomp($key) unless delete $stash->{no_trim};
524 2         81 return $key;
525             }
526              
527             # Preferred key is a subroutine
528 0         0 elsif (ref $index eq 'CODE') {
529              
530 50         217 local $_ = $c->localize;
531 50         940 my $pref = $index->($c);
532 50 100       435 return ref $pref ? @$pref : ($pref);
533             }
534              
535             # Preferred key is an array
536             elsif (ref $index eq 'ARRAY') {
537             return @{$index};
538             };
539              
540             # No preferred keys or invalid notation
541 0           return;
542             };
543              
544              
545             1;
546              
547              
548             __END__