File Coverage

blib/lib/Locale/Babelfish.pm
Criterion Covered Total %
statement 191 225 84.8
branch 62 100 62.0
condition 19 39 48.7
subroutine 32 34 94.1
pod 13 13 100.0
total 317 411 77.1


line stmt bran cond sub pod time code
1             package Locale::Babelfish;
2              
3             # ABSTRACT: Perl I18n using https://github.com/nodeca/babelfish format.
4              
5             our $VERSION = '2.13'; # VERSION
6              
7              
8 3     3   1274535 use utf8;
  3         383  
  3         54  
9 3     3   127 use strict;
  3         7  
  3         85  
10 3     3   15 use warnings;
  3         7  
  3         193  
11 3     3   695 use Data::Dumper;
  3         10314  
  3         355  
12              
13 3     3   27 use Carp qw/ confess /;
  3         4  
  3         225  
14 3     3   18 use File::Find qw( find );
  3         5  
  3         171  
15 3     3   16 use File::Spec ();
  3         7  
  3         62  
16              
17 3     3   2004 use YAML::SyckWrapper qw( load_yaml );
  3         184569  
  3         251  
18 3     3   2110 use Locale::Babelfish::Phrase::Parser ();
  3         12  
  3         127  
19 3     3   23 use Locale::Babelfish::Phrase::Compiler ();
  3         5  
  3         83  
20              
21              
22 3     3   14 use parent qw( Class::Accessor::Fast );
  3         6  
  3         17  
23              
24             use constant {
25 3         11436 MTIME_INDEX => 9,
26 3     3   213 };
  3         7  
27              
28             __PACKAGE__->mk_accessors( qw(
29             dictionaries
30             fallbacks
31             fallback_cache
32             dirs
33             suffix
34             default_locale
35             watch
36             watchers
37             ) );
38              
39             my $parser = Locale::Babelfish::Phrase::Parser->new();
40             my $compiler = Locale::Babelfish::Phrase::Compiler->new();
41              
42              
43             sub _built_config {
44 2     2   6 my ( $cfg ) = @_;
45             return {
46             dictionaries => {},
47             fallbacks => {},
48             fallback_cache => {},
49             suffix => $cfg->{suffix} // 'yaml',
50             default_locale => $cfg->{default_locale} // 'en_US',
51             watch => $cfg->{watch} || 0,
52             watchers => {},
53 2   50     30 %{ $cfg // {} },
  2   50     48  
      50        
      50        
54             };
55             }
56              
57             sub new {
58 2     2 1 1767 my ( $class, $cfg ) = @_;
59              
60             my $self = bless {
61             _cfg => $cfg,
62 2         4 %{ _built_config( $cfg ) },
  2         8  
63             }, $class;
64              
65 2         13 $self->load_dictionaries;
66 2         11 $self->locale( $self->{default_locale} );
67              
68 2         20 return $self;
69             }
70              
71              
72             sub locale {
73 5     5 1 4258 my $self = shift;
74 5 50       19 return $self->{locale} if scalar(@_) == 0;
75 5         20 $self->{locale} = $self->detect_locale( $_[0] );
76             }
77              
78              
79             sub on_watcher_change {
80 0     0 1 0 my ( $self ) = @_;
81 0         0 delete $self->{keys %$self};
82 0         0 my %new_cfg = %{ _built_config( $self->{_cfg} ) };
  0         0  
83 0         0 while( my ( $key, $value ) = each %new_cfg ) {
84 0         0 $self->{$key} = $value;
85             }
86 0         0 $self->load_dictionaries;
87 0         0 $self->locale( $self->{default_locale} );
88             }
89              
90              
91             sub look_for_watchers {
92 0     0 1 0 my ( $self ) = @_;
93 0 0       0 return unless $self->{watch};
94 0         0 my $ok = 1;
95 0         0 while ( my ( $file, $mtime ) = each %{ $self->watchers } ) {
  0         0  
96 0         0 my $new_mtime = (stat($file))[MTIME_INDEX];
97 0 0 0     0 if ( !defined( $mtime ) || !defined( $new_mtime ) || $new_mtime != $mtime ) {
      0        
98 0         0 $ok = 0;
99 0         0 last;
100             }
101             }
102 0 0       0 return if $ok;
103 0         0 $self->on_watcher_change();
104             }
105              
106              
107             sub t_or_undef {
108 48     48 1 160 my ( $self, $dictname_key, $params, $custom_locale ) = @_;
109              
110 48 50       181 confess 'No dictname_key' unless $dictname_key;
111             # запрещаем ключи не ASCII
112 48 50       282 confess("wrong dictname_key: $dictname_key") if $dictname_key =~ m/\P{ASCII}/;
113              
114 48 50       190 my $locale = $custom_locale ? $self->detect_locale( $custom_locale ) : $self->{locale};
115 48         209 my $r = $self->{dictionaries}->{$locale}->{$dictname_key};
116              
117 48 100       204 if ( defined $r ) {
118 45 100       291 if ( ref( $r ) eq 'SCALAR' ) {
    100          
119 11         60 $self->{dictionaries}->{$locale}->{$dictname_key} = $r = $compiler->compile(
120             $parser->parse( $$r, $locale ),
121             );
122             }
123             elsif ( ref( $r ) eq 'ARRAY' ) {
124 25         98 $self->{dictionaries}{$locale}{$dictname_key} = $r = _process_list_items( $r, $locale );
125             }
126             }
127             # fallbacks
128             else {
129 3   100     21 $self->{fallback_cache}->{$locale} //= {};
130             # в кэше может быть undef, чтобы не пробегать локали для несуществующих ключей повторно.
131 3 50       11 if ( exists $self->{fallback_cache}->{$locale}->{$dictname_key} ) {
132 0         0 $r = $self->{fallback_cache}->{$locale}->{$dictname_key};
133             }
134             else {
135 3   100     71 my @fallback_locales = @{ $self->{fallbacks}->{$locale} // [] };
  3         24  
136 3         9 for ( @fallback_locales ) {
137 2         8 $r = $self->{dictionaries}->{$_}->{$dictname_key};
138 2 100       9 if ( defined $r ) {
139 1 50       26 if ( ref( $r ) eq 'SCALAR' ) {
    0          
140 1         9 $self->{dictionaries}->{$_}->{$dictname_key} = $r = $compiler->compile(
141             $parser->parse( $$r, $_ ),
142             );
143             }
144             elsif ( ref( $r ) eq 'ARRAY' ) {
145 0         0 $self->{dictionaries}{$locale}{$dictname_key} = $r
146             = _process_list_items( $r, $locale );
147             }
148 1         4 last;
149             }
150             }
151 3         14 $self->{fallback_cache}->{$locale}->{$dictname_key} = $r;
152             }
153             }
154              
155 48 100       268 if ( ref( $r ) eq 'CODE' ) {
156 43         103 my $flat_params = {};
157             # Переводим хэш параметров в "плоскую форму" так как в babelfish они имеют вид params.key.subkey
158 43 100       131 if ( defined($params) ) {
159             # переданный скаляр превращаем в хэш { count, value }.
160 36 100       189 if ( ref($params) eq '' ) {
161 3         12 $flat_params = {
162             count => $params,
163             value => $params,
164             };
165             }
166             else {
167 33         149 _flat_hash_keys( $params, '', $flat_params );
168             }
169             }
170              
171 43         679 return $r->( $flat_params );
172             }
173              
174 5         41 return $r;
175             }
176              
177              
178             sub t {
179 48     48 1 75248 my $self = shift;
180              
181 48   66     218 return $self->t_or_undef( @_ ) || "[$_[0]]";
182             }
183              
184              
185             sub has_any_value {
186 1     1 1 3 my ( $self, $dictname_key, $custom_locale ) = @_;
187              
188             # запрещаем ключи не ASCII
189 1 50       73 confess("wrong dictname_key: $dictname_key") if $dictname_key =~ m/\P{ASCII}/;
190              
191 1 50       6 my $locale = $custom_locale ? $self->detect_locale( $custom_locale ) : $self->{locale};
192              
193 1 50       7 return 1 if $self->{dictionaries}->{$locale}->{$dictname_key};
194              
195 0   0     0 $self->{fallback_cache}->{$locale} //= {};
196             return ( ( defined $self->{fallback_cache}->{$locale}->{$dictname_key} ) ? 1 : 0 )
197 0 0       0 if exists $self->{fallback_cache}->{$locale}->{$dictname_key};
    0          
198              
199 0   0     0 my @fallback_locales = @{ $self->{fallbacks}->{$locale} // [] };
  0         0  
200 0         0 for ( @fallback_locales ) {
201 0 0       0 return 1 if defined $self->{dictionaries}->{$_}->{$dictname_key};
202             }
203              
204             }
205              
206              
207             sub load_dictionaries {
208 2     2 1 3 my $self = shift;
209              
210 2         4 for my $dir ( @{$self->dirs} ) {
  2         76  
211 2         86 my $fdir = File::Spec->rel2abs( $dir );
212             find( {
213             follow => 1,
214             no_chdir => 1,
215             wanted => sub {
216 28     28   1196 my $file = File::Spec->rel2abs( $File::Find::name );
217 28 100       1969 return unless -f $file;
218 16         215 my ( $volume, $directories, $base ) = File::Spec->splitpath( $file );
219              
220 16         60 my @tmp = split m/\./, $base;
221              
222 16         33 my $cur_suffix = pop @tmp;
223 16 50       411 return unless $cur_suffix eq $self->suffix;
224 16         101 my $lang = pop @tmp;
225              
226 16 50       34 pop @tmp if $tmp[-1] eq 'tt'; # словари вида formatting.tt.ru_RU.yaml - имеют имя formatting
227 16 50       29 if ( $tmp[-1] eq 'js') {
228             # словари .js перекрывают одноимённые словари без суффикса
229             # если это нежелательное поведение - словарь с суффиксом .tt перекроет одноимённый .js, и будет доступен только на сервере
230 0         0 pop @tmp; # словари вида formatting.js.ru_RU.yaml - имеют имя formatting
231             # и не загружаются, если есть аналогичный tt.
232 0 0       0 return if -f File::Spec->catpath( $volume, $directories, join('.', @tmp). ".tt.$lang.$cur_suffix" );
233             }
234 16         38 my $dictname = join('.', @tmp);
235 16         115 my $subdir = File::Spec->catpath( $volume, $directories, '' );
236 16 100       204 if ( $subdir =~ m/\A\Q$fdir\E[\\\/](.+)\z/ ) {
237 12         37 $dictname = "$1$dictname";
238             }
239              
240 16         42 $self->load_dictionary($dictname, $lang, $file);
241             },
242 2         380 }, $dir );
243             }
244 2         72 $self->prepare_to_compile;
245             }
246              
247              
248             sub load_dictionary {
249 16     16 1 52 my ( $self, $dictname, $lang, $file ) = @_;
250              
251 16   100     332 $self->dictionaries->{$lang} //= {};
252              
253 16         168 my $yaml = load_yaml( $file );
254              
255 16         11540 _flat_hash_keys( $yaml, "$dictname.", $self->dictionaries->{$lang} );
256              
257 16 50       341 return unless $self->watch;
258              
259 0         0 $self->watchers->{$file} = (stat($file))[MTIME_INDEX];
260             }
261              
262              
263             sub phrase_need_compilation {
264 90     90 1 134 my ( undef, $phrase, $key ) = @_;
265 90 50       145 die "L10N: $key is undef" unless defined $phrase;
266 90   100     409 return 1
267             && ref($phrase) eq ''
268             && $phrase =~ m/ (?: \(\( | \#\{ | \\\\ )/x
269             ;
270             }
271              
272              
273              
274             sub prepare_to_compile {
275 2     2 1 6 my ( $self ) = @_;
276 2         98 while ( my ($lang, $dic) = each(%{ $self->{dictionaries} }) ) {
  6         25  
277 4         15 while ( my ($key, $value) = each(%$dic) ) {
278 90 100       143 if ( $self->phrase_need_compilation( $value, $key ) ) {
279 30         95 $dic->{$key} = \$value; # отложенная компиляция
280             #my $ast = $parser->parse($value, $lang);
281             #$dic->{$key} = $compiler->compile( $ast );
282             }
283             }
284             }
285 2         6 return 1;
286             }
287              
288              
289             sub detect_locale {
290 7     7 1 16 my ( $self, $locale ) = @_;
291 7 100       216 return $locale if $self->dictionaries->{$locale};
292 2         15 my @alt_locales = grep { $_ =~ m/\A\Q$locale\E[\-_]/i } keys %{ $self->dictionaries };
  4         88  
  2         40  
293 2 50       9 confess "only one alternative locale allowed: ", join ',', @alt_locales
294             if @alt_locales > 1;
295              
296 2         6 my $alt_locale = $alt_locales[0];
297 2 50 33     52 if ( $alt_locale && $self->dictionaries->{$alt_locale} ) {
298             # сделаем locale dictionary ссылкой на alt locale dictinary.
299             # это ускорит работу всех t с указанием языка типа "ru" вместо локали "ru_RU".
300 2         45 $self->dictionaries->{$locale} = $self->dictionaries->{$alt_locale};
301              
302             $self->fallback_cache->{$locale} = $self->fallback_cache->{$alt_locale}
303 2 50       47 if exists $self->fallback_cache->{$alt_locale};
304              
305             $self->fallbacks->{$locale} = $self->fallbacks->{$alt_locale}
306 2 50       44 if exists $self->fallbacks->{$alt_locale};
307              
308 2         73 return $locale;
309             }
310 0 0       0 return $self->{default_locale} if $self->dictionaries->{ $self->{default_locale} };
311 0         0 confess "bad locale: $locale and bad default_locale: $self->{default_locale}.";
312             }
313              
314              
315             sub set_fallback {
316 2     2 1 15 my ( $self, $locale, @fallback_locales ) = @_;
317 2 50       8 return unless scalar( @fallback_locales );
318              
319 2         9 $locale = $self->detect_locale( $locale );
320              
321 2 50 33     24 @fallback_locales = @{ $fallback_locales[0] } if 1
  0         0  
322             && scalar( @fallback_locales ) == 1
323             && ref( $fallback_locales[0] ) eq 'ARRAY'
324             ;
325              
326 2         40 $self->fallbacks->{ $locale } = \@fallback_locales;
327 2         12 delete $self->{fallback_cache}->{ $locale };
328              
329 2         13 return 1;
330             }
331              
332              
333             sub _flat_hash_keys {
334 79     79   287 my ( $hash, $prefix, $store ) = @_;
335 79         342 while ( my ($key, $value) = each(%$hash) ) {
336 173 100       353 if (ref($value) eq 'HASH') {
337 30         95 _flat_hash_keys( $value, "$prefix$key.", $store );
338             } else {
339 143         703 $store->{"$prefix$key"} = $value;
340             }
341             }
342 79         236 return 1;
343             }
344              
345              
346             sub _process_list_items {
347 25     25   73 my ( $r, $locale ) = @_;
348              
349 25         50 my @compiled_items;
350 25         57 for my $item ( @{ $r } ) {
  25         78  
351 42 100 66     170 if ( ref $item eq 'HASH' ) {
    100          
352 36         126 push @compiled_items, _process_nested_hash_item( $item, $locale );
353             }
354             elsif ( ref $item ne 'HASH' && defined $item ) {
355 4         22 push @compiled_items, $compiler->compile( $parser->parse( $item, $locale ) );
356             }
357             else {
358 2         7 push @compiled_items, $item;
359             }
360             }
361              
362             return sub {
363 26     26   64 my $results = [];
364 26         81 my @params = @_;
365              
366 26         66 for my $item ( @compiled_items ) {
367 44         84 push @{ $results }, __get_compiled_value($item, @params);
  44         139  
368             }
369 26         322 return $results;
370              
371             sub __get_compiled_value {
372 120     120   268 my ($item, @params) = @_;
373 120         222 my $result;
374              
375 120 100       370 if ( ref( $item ) eq 'CODE' ) {
    100          
376 16         682 $result = $item->(@params);
377             }
378             # Нужно скомпилить значения в хэшрефе
379             elsif ( ref( $item ) eq 'HASH' ) {
380 66         125 my $new_item = {};
381 66         272 while ( my ( $key, $value ) = each ( %$item ) ) {
382 160 100       450 if ( ref ($value) eq 'CODE' ) {
    100          
    100          
383 36         1302 $new_item->{$key} = $value->(@params);
384             }
385             elsif ( ref( $value ) eq 'HASH' ) {
386 16         35 my $sub_item = {};
387 16         82 foreach my $sub_key (keys %$value) {
388 48         123 $sub_item->{$sub_key} = __get_compiled_value($value->{$sub_key}, @params );
389             }
390 16         81 $new_item->{$key} = $sub_item;
391             }
392             elsif ( ref( $value ) eq 'ARRAY' ) {
393 16         30 my $sub_item = [];
394 16         43 foreach my $sub_value (@$value) {
395 28         111 push @$sub_item, __get_compiled_value($sub_value, @params );
396             }
397 16         70 $new_item->{$key} = $sub_item;
398             }
399             else {
400 92         387 $new_item->{$key} = $value;
401             }
402             }
403 66         134 $result = $new_item;
404             }
405             else {
406 38         65 $result = $item;
407             }
408            
409 120         404 return $result;
410             }
411              
412 0         0 return $results;
413 25         439 };
414              
415             }
416              
417             sub _process_nested_hash_item {
418 80     80   240 my ( $hashref, $locale ) = @_;
419              
420 80         377 while ( my ( $key, $value ) = each ( %$hashref ) ) {
421 206         497 my $compiled_value = _process_nested_item_value( $value, $locale );
422 206         1540 $hashref->{$key} = $compiled_value;
423             }
424              
425 80         326 return $hashref;
426             }
427              
428             sub _process_nested_array_item {
429 16     16   43 my ( $arrayref, $locale ) = @_;
430 16         43 my $arrayref_out = [];
431              
432 16         40 foreach my $value (@$arrayref ) {
433 28         121 my $compiled_value = _process_nested_item_value( $value, $locale );
434 28         94 push @$arrayref_out, $compiled_value;
435             }
436              
437 16         43 return $arrayref_out;
438             }
439              
440             sub _process_nested_item_value {
441 234     234   505 my ( $item_value, $locale ) = @_;
442            
443 234 100       1414 my $compiled_value = ref $item_value eq 'HASH' ? _process_nested_hash_item( $item_value, $locale ) :
    100          
444             ref $item_value eq 'ARRAY' ? _process_nested_array_item( $item_value, $locale ) :
445             $compiler->compile( $parser->parse( $item_value, $locale ) );
446              
447 234         4487 return $compiled_value;
448             }
449              
450              
451              
452             1;
453              
454             __END__
455              
456             =pod
457              
458             =encoding utf-8
459              
460             =head1 NAME
461              
462             Locale::Babelfish - Perl I18n using https://github.com/nodeca/babelfish format.
463              
464             =head1 VERSION
465              
466             version 2.13
467              
468             =head1 DESCRIPTION
469              
470             Библиотека локализации.
471              
472             =head1 NAME
473              
474             Locale::Babelfish
475              
476             =head1 SYNOPSYS
477              
478             package Foo;
479              
480             use Locale::Babelfish ();
481              
482             my $bf = Locale::Babelfish->new( { dirs => [ '/path/to/dictionaries' ] } );
483             print $bf->t('dictionary.firstkey.nextkey', { foo => 'bar' } );
484              
485             More sophisticated example:
486              
487             package Foo::Bar;
488              
489             use Locale::Babelfish ();
490              
491             my $bf = Locale::Babelfish->new( {
492             # configuration
493             dirs => [ '/path/to/dictionaries' ],
494             default_locale => [ 'ru_RU' ], # By default en_US
495             } );
496              
497             # using default locale
498             print $bf->t( 'dictionary.akey' );
499             print $bf->t( 'dictionary.firstkey.nextkey', { foo => 'bar' } );
500              
501             # using specified locale
502             print $bf->t( 'dictionary.firstkey.nextkey', { foo => 'bar' }, 'by_BY' );
503              
504             # using scalar as count or value variable
505             print $bf->t( 'dictionary.firstkey.nextkey', 90 );
506             # same as
507             print $bf->t( 'dictionary.firstkey.nextkey', { count => 90, value => 90 } );
508              
509             # set locale
510             $bf->locale( 'en_US' );
511             print $bf->t( 'dictionary.firstkey.nextkey', { foo => 'bar' } );
512              
513             # Get current locale
514             print $bf->locale;
515              
516             =head1 DICTIONARIES
517              
518             =head2 Phrases Syntax
519              
520             #{varname} Echoes value of variable
521             ((Singular|Plural1|Plural2)):variable Plural form
522             ((Singular|Plural1|Plural2)) Short plural form for "count" variable
523              
524             Example:
525              
526             I have #{nails_count} ((nail|nails)):nails_count
527              
528             or short form
529              
530             I have #{count} ((nail|nails))
531              
532             or with zero and onу plural forms:
533              
534             I have ((=0 no nails|=1 a nail|#{nails_count} nail|#{nails_count} nails)):nails_count
535              
536             =head2 Dictionary file example
537              
538             Module support only YAML format. Create dictionary file like: B<dictionary.en_US.yaml> where
539             C<dictionary> is name of dictionary and C<en_US> - its locale.
540              
541             profile:
542             apps:
543             forums:
544             new_topic: New topic
545             last_post:
546             title : Last message
547             demo:
548             apples: I have #{count} ((apple|apples))
549             list:
550             - some content #{data}
551             - some other content #{data}
552              
553             =head1 DETAILS
554              
555             Словари грузятся при создании экземпляра, сразу в плоской форме
556             $self->{dictionaries}->{ru_RU}->{dictname_key}...
557              
558             Причем все скалярные значения, при необходимости (есть спецсимволы Babelfish),
559             преобразуются в ссылки на скаляры (флаг - "нужно скомпилировать").
560              
561             Метод t_or_undef получает значение по указанному ключу.
562              
563             Если это ссылка на скаляр, то парсит и компилирует строку.
564              
565             Если это ссылка на массив, то работаем со всеми элементами массива как со скалярами,
566             собираем полученные результаты компиляции в новый массив и возвращаем ссылку на этот массив.
567              
568             Результат компиляции либо ссылка на подпрограмму, либо просто строка.
569              
570             Если это ссылка на подпрограмму, мы просто вызываем ее с плоскими параметрами.
571              
572             Если просто строка, то возвращаем её as is.
573              
574             Поддерживается опция watch.
575              
576             =head1 METHODS
577              
578             =over
579              
580             =item locale
581              
582             Если указана локаль, устанавливет её. Если нет - возвращает.
583              
584             =item on_watcher_change
585              
586             Перечитывает все словари.
587              
588             =item look_for_watchers
589              
590             Обновляет словари оп мере необходимости, через L</on_watcher_change>.
591              
592             =item t_or_undef
593              
594             $self->t_or_undef( 'main.key.subkey' , { paaram1 => 1 , param2 => 'test' } , 'ru' );
595              
596             Локализация по ключу.
597              
598             первой частью в ключе $key должен идти словарь, например, main.key
599             параметр языка не обязательный.
600              
601             $params - хэш параметров
602              
603             =item t
604              
605             $self->t( 'main.key.subkey' , { paaram1 => 1 , param2 => 'test' } , 'ru' );
606              
607             Локализация по ключу.
608              
609             первой частью в ключе $key должен идти словарь, например, main.key
610             параметр языка не обязательный.
611              
612             $params - хэш параметров
613              
614             =item has_any_value
615              
616             $self->has_any_value( 'main.key.subkey' );
617              
618             Проверяет есть ли ключ в словаре
619              
620             первой частью в ключе должен идти словарь, например, main.
621              
622             =item load_dictionaries
623              
624             Загружает все yaml словари с диска
625              
626             =item load_dictionary
627              
628             Загружает один yaml словарь с диска
629              
630             =item phrase_need_compilation
631              
632             $self->phrase_need_compilation( $phrase, $key )
633             $class->phrase_need_compilation( $phrase, $key )
634              
635             Определяет, требуется ли компиляция фразы.
636              
637             Используется также при компиляции плюралов (вложенные выражения).
638              
639             =item prepare_to_compile
640              
641             $self->prepare_to_compile()
642              
643             Либо маркирует как refscalar строки в словарях, требующие компиляции,
644             либо просто компилирует их.
645              
646             =item detect_locale
647              
648             $self->detect_locale( $locale );
649              
650             Определяем какой язык будет использован.
651             приоритет $locale, далее default_locale.
652              
653             =item set_fallback
654              
655             $self->set_fallback( 'by_BY', 'ru_RU', 'en_US');
656             $self->set_fallback( 'by_BY', [ 'ru_RU', 'en_US' ] );
657              
658             Для указанной локали устанавливает список локалей, на которые будет производится откат
659             в случае отсутствия фразы в указанной.
660              
661             Например, в вышеуказанных примерах при отсутствии фразы в
662             белорусской локали будет затем искаться фраза в русской локали,
663             затем в англоамериканской.
664              
665             =item _flat_hash_keys
666              
667             _flat_hash_keys( $hash, '', $result );
668              
669             Внутренняя, рекурсивная.
670             Преобразует хэш любой вложенности в строку, где ключи хешей разделены точками.
671              
672             =item _process_list_items
673              
674             _process_list_items( $dictionary_values);
675              
676             Обрабатывает ключи словарей содержащие списки, и оборачивает в функцию для компиляции списка.
677             Поддерживаются вложенные структуры в виде hashref и arrayref
678              
679             =back
680              
681             =head1 AUTHORS
682              
683             =over 4
684              
685             =item *
686              
687             Akzhan Abdulin <akzhan@cpan.org>
688              
689             =item *
690              
691             Igor Mironov <grif@cpan.org>
692              
693             =item *
694              
695             Victor Efimov <efimov@reg.ru>
696              
697             =item *
698              
699             REG.RU LLC
700              
701             =item *
702              
703             Kirill Sysoev <k.sysoev@me.com>
704              
705             =item *
706              
707             Alexandr Tkach <tkach@reg.ru>
708              
709             =back
710              
711             =head1 COPYRIGHT AND LICENSE
712              
713             This software is Copyright (c) 2014 by REG.RU LLC.
714              
715             This is free software, licensed under:
716              
717             The MIT (X11) License
718              
719             =cut