File Coverage

blib/lib/Jenkins/i18n.pm
Criterion Covered Total %
statement 165 176 93.7
branch 71 94 75.5
condition 11 24 45.8
subroutine 22 22 100.0
pod 9 9 100.0
total 278 325 85.5


line stmt bran cond sub pod time code
1             package Jenkins::i18n;
2              
3 8     8   542293 use 5.014004;
  8         111  
4 8     8   44 use strict;
  8         16  
  8         178  
5 8     8   49 use warnings;
  8         13  
  8         280  
6 8     8   53 use Carp qw(confess);
  8         36  
  8         437  
7 8     8   55 use File::Find;
  8         24  
  8         602  
8 8     8   49 use File::Spec;
  8         14  
  8         229  
9 8     8   3343 use Set::Tiny;
  8         9642  
  8         431  
10              
11 8     8   3369 use Jenkins::i18n::Properties;
  8         24  
  8         304  
12 8     8   3312 use Jenkins::i18n::FindResults;
  8         32  
  8         331  
13 8     8   3295 use Jenkins::i18n::Assertions qw(is_jelly_file has_empty);
  8         19  
  8         506  
14              
15             =pod
16              
17             =head1 NAME
18              
19             Jenkins::i18n - functions for the jtt CLI
20              
21             =head1 SYNOPSIS
22              
23             use Jenkins::i18n qw(remove_unused find_files load_properties load_jelly find_langs);
24              
25             =head1 DESCRIPTION
26              
27             C is a CLI program used to help translating the Jenkins properties file.
28              
29             This module implements some of the functions used by the CLI.
30              
31             =cut
32              
33 8     8   54 use Exporter 'import';
  8         16  
  8         18511  
34             our @EXPORT_OK = (
35             'remove_unused', 'find_files', 'load_properties', 'load_jelly',
36             'find_langs', 'all_data', 'dump_keys', 'merge_data',
37             'find_missing'
38             );
39              
40             our $VERSION = '0.09';
41              
42             =head1 EXPORT
43              
44             None by default.
45              
46             =head1 FUNCTIONS
47              
48             =head2 find_missing
49              
50             Compares the keys available from the source (Jelly and/or Properties files)
51             with the i18n file and updates the statistics based on the B,
52             i.e., the keys that exists in the source but not in the i18n Properties file.
53              
54             Expects as parameters the following:
55              
56             =over
57              
58             =item 1.
59              
60             a hash reference with all the keys/values from the Jelly/Properties file.
61              
62             =item 2.
63              
64             a hash reference with all the keys/values from the i18n Properties file.
65              
66             =item 3.
67              
68             a instance of a L class.
69              
70             =item 4.
71              
72             a instance of L class.
73              
74             =back
75              
76             =cut
77              
78             sub find_missing {
79 2     2 1 13 my ( $source_ref, $i18n_ref, $stats, $warnings ) = @_;
80              
81 2         2 foreach my $entry ( keys %{$source_ref} ) {
  2         8  
82 6         18 $stats->add_key($entry);
83              
84             # TODO: skip increasing missing if operation is to delete those
85 6 50 33     55 unless (( exists( $i18n_ref->{$entry} ) )
86             and ( defined( $i18n_ref->{$entry} ) ) )
87             {
88 0         0 $stats->inc_missing;
89 0         0 $warnings->add( 'missing', $entry );
90 0         0 next;
91             }
92              
93 6 50       19 if ( $i18n_ref->{$entry} eq '' ) {
94 0 0       0 unless ( has_empty($entry) ) {
95 0         0 $stats->inc('empty');
96 0         0 $warnings->add( 'empty', $entry );
97             }
98             else {
99 0         0 $warnings->add( 'ignored', $entry );
100             }
101             }
102             }
103             }
104              
105             =head2 merge_data
106              
107             Merges the translation data from a Jelly file and a Properties file.
108              
109             Expects as parameters:
110              
111             =over
112              
113             =item 1.
114              
115             A hash reference with all the keys/values from a Jelly file.
116              
117             =item 2.
118              
119             A hash reference with all the keys/values from a Properties file.
120              
121             =back
122              
123             This methods considers the way Jenkins is translated nowadays, considering
124             different scenarios where the Jelly and Properties have different data.
125              
126             Returns a hash reference with the keys and values merged.
127              
128             =cut
129              
130             sub merge_data {
131 10     10 1 12889 my ( $jelly_ref, $properties_ref ) = @_;
132 10 100       55 confess('A hash reference of the Jelly keys is required')
133             unless ($jelly_ref);
134 9 100       43 confess('The Jelly type is invalid') unless ( ref($jelly_ref) eq 'HASH' );
135 8 100       28 confess('A hash reference of the Properties keys is required')
136             unless ($properties_ref);
137 7 100       27 confess('The Properties type is invalid')
138             unless ( ref($properties_ref) eq 'HASH' );
139 6         8 my %merged;
140              
141 6 100       11 if ( scalar( keys( %{$jelly_ref} ) ) == 0 ) {
  6         20  
142 1         4 return $properties_ref;
143             }
144              
145 5         7 foreach my $prop_key ( keys( %{$jelly_ref} ) ) {
  5         16  
146 15 100       30 if ( exists( $properties_ref->{$prop_key} ) ) {
147 7         16 $merged{$prop_key} = $properties_ref->{$prop_key};
148             }
149             else {
150 8         18 $merged{$prop_key} = $prop_key;
151             }
152             }
153 5         14 return \%merged;
154             }
155              
156             =head2 dump_keys
157              
158             Prints to C all keys from a hash, using some formatting to make it
159             easier to read.
160              
161             Expects as parameter a hash reference.
162              
163             =cut
164              
165             sub dump_keys {
166 6     6 1 10 my $entries_ref = shift;
167 6         10 foreach my $key ( keys( %{$entries_ref} ) ) {
  6         26  
168 14         1368 print "\t$key\n";
169             }
170             }
171              
172             =head2 all_data
173              
174             Retrieves all translation data from a single given file as reference.
175              
176             Expects as parameter a complete path to a file.
177              
178             This file can be a Properties or Jelly file. From that file name, it will be
179             defined the related other files, by convention.
180              
181             Returns a array reference, where each index is:
182              
183             =over
184              
185             =item 1.
186              
187             A hash reference with all keys/values for the English language.
188              
189             =item 2.
190              
191             A hash reference with all the keys/values for the related language.
192              
193             =item 3.
194              
195             A hash reference for the keys retrieved from the respective Jelly file.
196              
197             =back
198              
199             Any of the return references may point to an empty hash, but at list the first
200             reference must point to a non-empty hash.
201              
202             =cut
203              
204             sub all_data {
205 3     3 1 655 my ( $file, $processor ) = @_;
206 3 100       12 print "#####\nWorking on $file\n" if ( $processor->is_debug );
207 3         18 my ( $curr_lang_file, $english_file, $jelly_file )
208             = $processor->define_files($file);
209              
210 3 100       11 if ( $processor->is_debug ) {
211 2         359 print "For file $file:\n",
212             "\tthe localization file is $curr_lang_file\n",
213             "\tand the source is $english_file\n";
214             }
215              
216             # entries_ref -> keys used in jelly or Message.properties files
217             # lang_entries_ref -> keys/values in the desired language which are already
218             # present in the file
219 3         31 my ( $jelly_entries_ref, $lang_entries_ref, $english_entries_ref );
220              
221 3 50       57 if ( -f $jelly_file ) {
222 3         12 $jelly_entries_ref = load_jelly($jelly_file);
223             }
224             else {
225 0         0 $jelly_entries_ref = {};
226             }
227              
228 3         19 $english_entries_ref
229             = load_properties( $english_file, $processor->is_debug );
230 3         85 $lang_entries_ref
231             = load_properties( $curr_lang_file, $processor->is_debug );
232              
233 3 100       75 if ( $processor->is_debug ) {
234 2         252 print "All keys retrieved from $jelly_file:\n";
235 2         13 dump_keys($jelly_entries_ref);
236 2         218 print "All keys retrieved from $english_file:\n";
237 2         12 dump_keys($english_entries_ref);
238 2         246 print "All keys retrieved from $curr_lang_file:\n";
239 2         11 dump_keys($lang_entries_ref);
240             }
241              
242 3         22 return ( $jelly_entries_ref, $lang_entries_ref, $english_entries_ref );
243             }
244              
245             =head2 remove_unused
246              
247             Remove unused keys from a properties file.
248              
249             Each translation in every language depends on the original properties files
250             that are written in English.
251              
252             This function gets a set of keys and compare with those that are stored in the
253             translation file: anything that exists outside the original set in English is
254             considered deprecated and so removed.
255              
256             Expects as positional parameters:
257              
258             =over
259              
260             =item 1.
261              
262             file: the complete path to the translation file to be checked.
263              
264             =item 2.
265              
266             keys: a L instance of the keys from the original English properties
267             file.
268              
269             =item 3.
270              
271             license: a scalar reference with a license to include the header of the
272             translated properties file.
273              
274             =item 4
275              
276             backup: a boolean (0 or 1) if a backup file should be created in the same path
277             of the file parameter. Optional.
278              
279             =back
280              
281             Returns the number of keys removed (as an integer).
282              
283             =cut
284              
285             sub remove_unused {
286 6     6 1 9710 my $file = shift;
287 6 100       34 confess "file is a required parameter\n" unless ( defined($file) );
288 5         13 my $keys = shift;
289 5 100       19 confess "keys is a required parameter\n" unless ( defined($keys) );
290 4 100       23 confess "keys must be a Set::Tiny instance\n"
291             unless ( ref($keys) eq 'Set::Tiny' );
292 3         7 my $license_ref = shift;
293 3 100       20 confess "license must be an array reference"
294             unless ( ref($license_ref) eq 'ARRAY' );
295 2         4 my $use_backup = shift;
296 2 100       6 $use_backup = 0 unless ( defined($use_backup) );
297              
298 2         3 my $props_handler;
299              
300 2 100       7 if ($use_backup) {
301 1         6 my $backup = "$file.bak";
302 1 50       85 rename( $file, $backup )
303             or confess "Cannot rename $file to $backup: $!\n";
304 1         10 $props_handler = Jenkins::i18n::Properties->new( file => $backup );
305             }
306             else {
307 1         9 $props_handler = Jenkins::i18n::Properties->new( file => $file );
308             }
309              
310 2         57 my $curr_keys = Set::Tiny->new( $props_handler->propertyNames );
311 2         245 my $to_delete = $curr_keys->difference($keys);
312              
313 2         72 foreach my $key ( $to_delete->members ) {
314 24         321 $props_handler->deleteProperty($key);
315             }
316              
317 2 50       224 open( my $out, '>', $file ) or confess "Cannot write to $file: $!\n";
318 2         16 $props_handler->save( $out, $license_ref );
319 2 50       158 close($out) or confess "Cannot save $file: $!\n";
320              
321 2         13 return $to_delete->size;
322             }
323              
324             =head2 find_files
325              
326             Find all Jelly and Java Properties files that could be translated from English,
327             i.e., files that do not have a ISO 639-1 standard language based code as a
328             filename prefix (before the file extension).
329              
330             Expects as parameters:
331              
332             =over
333              
334             =item 1.
335              
336             The complete path to a directory that might contain such files.
337              
338             =item 2.
339              
340             An instance of L with all the languages codes identified. See
341             C.
342              
343             =back
344              
345             Returns an L instance.
346              
347             =cut
348              
349             # Relative paths inside the Jenkins project repository
350             my $src_test_path = File::Spec->catfile( 'src', 'test' );
351             my $target_path = File::Spec->catfile( 'target', '' );
352             my $src_regex = qr/$src_test_path/;
353             my $target_regex = qr/$target_path/;
354             my $msgs_regex = qr/Messages\.properties$/;
355             my $jelly_regex = qr/\.jelly$/;
356             my $properties_regex = qr/\.properties$/;
357              
358             sub find_files {
359 5     5 1 4832 my ( $dir, $all_known_langs ) = @_;
360 5 100       33 confess 'Must provide a string, invalid directory parameter'
361             unless ($dir);
362 4 100       25 confess 'Must provide a string as directory, not a reference'
363             unless ( ref($dir) eq '' );
364 3 100       113 confess "Directory '$dir' must exist" unless ( -d $dir );
365 2 50       12 confess "Must receive a Set::Tiny instance for langs parameter"
366             unless ( ref($all_known_langs) eq 'Set::Tiny' );
367              
368 2         4 my $country_code_length = 2;
369 2         5 my $lang_code_length = 2;
370 2         5 my $min_file_pieces = 2;
371 2         16 my $under_regex = qr/_/;
372 2         17 my $result = Jenkins::i18n::FindResults->new;
373 2         15 $result->add_warning(
374             "Warning: ignoring the files at $src_test_path and $target_path paths."
375             );
376              
377             find(
378             sub {
379 13     13   35 my $file = $File::Find::name;
380              
381 13 50 33     116 unless ( ( $file =~ $src_regex ) or ( $file =~ $target_regex ) ) {
382 13 100 100     87 if ( ( $file =~ $msgs_regex )
383             or ( $file =~ $jelly_regex ) )
384             {
385 5         36 $result->add_file($file);
386             }
387             else {
388              
389 8 100       334 if ( $file =~ $properties_regex ) {
390 5         82 my $file_name = ( File::Spec->splitpath($file) )[-1];
391 5         27 $file_name =~ s/$properties_regex//;
392 5         23 my @pieces = split( $under_regex, $file_name );
393              
394             # we must ignore a "_" at the beginning of the file
395 5 50       24 shift @pieces if ( $pieces[0] eq '' );
396              
397 5 100       47 if ( scalar(@pieces) < $min_file_pieces ) {
398 2         19 $result->add_file($file);
399             }
400             else {
401 3 50 33     38 if (
    50 33        
      33        
402             ( scalar(@pieces) == $min_file_pieces )
403             and
404             ( length( $pieces[-1] ) == $lang_code_length )
405             )
406             {
407 0 0       0 $result->add_warning("Ignoring $file")
408             if (
409             $all_known_langs->member( $pieces[-1] ) );
410             }
411             elsif (
412             ( scalar(@pieces) > $min_file_pieces )
413             and (
414             length( $pieces[-1] )
415             == $country_code_length )
416             and
417             ( length( $pieces[-2] ) == $lang_code_length )
418             )
419             {
420 3 50       27 $result->add_warning("Ignoring $file")
421             if (
422             $all_known_langs->member(
423             $pieces[-2] . '_' . $pieces[-1]
424             )
425             );
426             }
427             else {
428 0         0 $result->add_file($file);
429             }
430             }
431             }
432             }
433             }
434             },
435 2         213 $dir
436             );
437 2         34 return $result;
438             }
439              
440             my $regex = qr/_([a-z]{2})(_[A-Z]{2})?\.properties$/;
441              
442             =head2 find_langs
443              
444             Finds all ISO 639-1 standard language based codes available in the Jenkins
445             repository based on the filenames sufix (before the file extension) of the
446             translated files.
447              
448             This is basically the opposite of C does.
449              
450             It expect as parameters the complete path to a directory to search for the
451             files.
452              
453             Returns a instance of the L class containing all the language codes
454             that were identified.
455              
456             Find all files Jelly and Java Properties files that could be translated from
457             English, i.e., files that do not have a ISO 639-1 standard language based code
458             as a filename prefix (before the file extension).
459              
460             =cut
461              
462             sub find_langs {
463 1     1 1 122 my $dir = shift;
464 1 50       4 confess 'Must provide a string, invalid directory parameter'
465             unless ($dir);
466 1 50       37 confess 'Must provide a string as directory, not a reference'
467             unless ( ref($dir) eq '' );
468 1 50       18 confess "Directory '$dir' must exist" unless ( -d $dir );
469 1         11 my $langs = Set::Tiny->new;
470              
471             find(
472             sub {
473 4     4   12 my $file = $File::Find::name;
474              
475 4 50 33     33 unless ( ( $file =~ $src_regex ) or ( $file =~ $target_regex ) ) {
476 4 100       156 if ( $file =~ $regex ) {
477 1         2 my $lang;
478              
479 1 50       6 if ($2) {
480 1         3 $lang = $1 . $2;
481             }
482             else {
483 0         0 $lang = $1;
484             }
485              
486 1         5 $langs->insert($lang);
487             }
488             }
489             },
490 1         136 $dir
491             );
492              
493 1         58 return $langs;
494             }
495              
496             =head2 load_properties
497              
498             Loads the content of a Java Properties file into a hash.
499              
500             Expects as position parameters:
501              
502             =over
503              
504             =item 1
505              
506             The complete path to a Java Properties file.
507              
508             =item 2
509              
510             True (1) or false (0) if a warn should be printed to C in case the file
511             is missing.
512              
513             =back
514              
515             Returns an hash reference with the file content. If the file doesn't exist,
516             returns an empty hash reference.
517              
518             =cut
519              
520             sub load_properties {
521 10     10 1 4033 my ( $file, $must_warn ) = @_;
522 10 50       27 confess 'The complete path to the properties file is required'
523             unless ($file);
524 10 100       38 confess 'Must pass if a warning is required or not'
525             unless ( defined($must_warn) );
526              
527 9 100       194 unless ( -f $file ) {
528 2 100       21 warn "File $file doesn't exist, skipping it...\n" if ($must_warn);
529 2         12 return {};
530             }
531              
532 7         87 my $props_handler = Jenkins::i18n::Properties->new( file => $file );
533 7         213 return $props_handler->getProperties;
534             }
535              
536             =head2 load_jelly
537              
538             Fill a hash with key/1 pairs from a C<.jelly> file.
539              
540             Expects as parameter the path to a Jelly file.
541              
542             Returns a hash reference.
543              
544             =cut
545              
546             # TODO: replace regex with XML parser
547             sub load_jelly {
548 5     5 1 2880 my $file = shift;
549 5         9 my %ret;
550              
551 5 50       233 open( my $fh, '<', $file ) or confess "Cannot read $file: $!\n";
552              
553 5         1117 while (<$fh>) {
554 220 100       2155 next if ( !/\$\{.*?\%([^\(]+?).*\}/ );
555 10         21 my $line = $_;
556 10   66     64 while ($line =~ /^.*?\$\{\%([^\(\}]+)(.*)$/
557             || $line =~ /^.*?\$\{.*?['"]\%([^\(\}\"\']+)(.*)$/ )
558             {
559 13         39 $line = $2;
560 13         22 my $word = $1;
561 13         25 $word =~ s/\(.+$//g;
562 13         19 $word =~ s/'+/''/g;
563 13         33 $word =~ s/ /\\ /g;
564 13         22 $word =~ s/\>/>/g;
565 13         52 $word =~ s/\</
566 13         23 $word =~ s/\&/&/g;
567 13         24 $word =~ s/([#:=])/\\$1/g;
568 13         123 $ret{$word} = 1;
569             }
570             }
571              
572 5         54 close($fh);
573 5         37 return \%ret;
574             }
575              
576             1;
577              
578             __END__