locales.pl strict machen -- Teil 1
[kivitendo-erp.git] / locale / de / locales.pl
1 #!/usr/bin/perl
2
3 # -n do not include custom_ scripts
4 # -v verbose mode, shows progress stuff
5
6 # this version of locles processes not only all required .pl files
7 # but also all parse_html_templated files.
8
9 use strict;
10
11 use Data::Dumper;
12 use English;
13 use FileHandle;
14 use Getopt::Long;
15 use List::Util qw(first);
16 use POSIX;
17 use Pod::Usage;
18
19 $OUTPUT_AUTOFLUSH = 1;
20
21 my $basedir      = "../..";
22 my $bindir       = "$basedir/bin/mozilla";
23 my $dbupdir      = "$basedir/sql/Pg-upgrade";
24 my $dbupdir2     = "$basedir/sql/Pg-upgrade2";
25 my $menufile     = "menu.ini";
26 my $submitsearch = qr/type\s*=\s*[\"\']?submit/i;
27
28 my (%referenced_html_files, %locale, %htmllocale, %alllocales, %cached, %submit, %subrt);
29
30 my $count  = 0;
31 my $notext = 0;
32
33 my $debug  = 0;
34
35 my $opt_v  = 0;
36 my $opt_n  = 0;
37 my $opt_c  = 0;
38
39 sub parse_args {
40   my ($help, $man);
41
42   GetOptions('no-custom-files' => \$opt_n,
43              'check-files'     => \$opt_c,
44              'verbose'         => \$opt_v,
45              'help'            => \$help,
46              'man'             => \$man,);
47
48   if ($help) {
49     pod2usage(1);
50     exit 0;
51   }
52
53   if ($man) {
54     pod2usage(-exitstatus => 0, -verbose => 2);
55     exit 0;
56   }
57 }
58
59 parse_args();
60
61 opendir DIR, "$bindir" or die "$!";
62 my @progfiles = grep { /\.pl$/ && !/(_|^\.)/ } readdir DIR;
63 seekdir DIR, 0;
64 my @customfiles = grep /_/, readdir DIR;
65 closedir DIR;
66
67 # put customized files into @customfiles
68 my @menufiles;
69
70 if ($opt_n) {
71   @customfiles = ();
72   @menufiles   = ($menufile);
73 } else {
74   opendir DIR, "$basedir" or die "$!";
75   @menufiles = grep { /.*?_$menufile$/ } readdir DIR;
76   closedir DIR;
77   unshift @menufiles, $menufile;
78 }
79
80 opendir DIR, $dbupdir or die "$!";
81 my @dbplfiles = grep { /\.pl$/ } readdir DIR;
82 closedir DIR;
83
84 opendir DIR, $dbupdir2 or die "$!";
85 my @dbplfiles2 = grep { /\.pl$/ } readdir DIR;
86 closedir DIR;
87
88 # slurp the translations in
89 my $self    = {};
90 my $missing = {};
91 my @missing = ();
92 my @lost   = ();
93
94 if (-f 'all') {
95   require 'all';
96 }
97 if (-f 'missing') {
98   require 'missing' ;
99   unlink 'missing';
100 }
101 if (-f 'lost') {
102   require 'lost';
103   unlink 'lost';
104 }
105
106 my %old_texts = %{ $self->{texts} || {} };
107
108 # Read HTML templates.
109 #%htmllocale = ();
110 #@htmltemplates = <../../templates/webpages/*/*_master.html>;
111 #foreach $file (@htmltemplates) {
112 #  scanhtmlfile($file);
113 #}
114
115 map({ handle_file($_, $bindir); } @progfiles);
116 map({ handle_file($_, $dbupdir); } @dbplfiles);
117 map({ handle_file($_, $dbupdir2); } @dbplfiles2);
118
119 sub handle_file {
120   my ($file, $dir) = @_;
121   print "\n$file" if $opt_v;
122   my %locale = ();
123   my %submit = ();
124   my %subrt  = ();
125
126   &scanfile("$dir/$file");
127
128   # scan custom_{module}.pl or {login}_{module}.pl files
129   foreach my $customfile (@customfiles) {
130     if ($customfile =~ /_$file/) {
131       if (-f "$dir/$customfile") {
132         &scanfile("$dir/$customfile");
133       }
134     }
135   }
136
137   # if this is the menu.pl file
138   if ($file eq 'menu.pl') {
139     foreach my $item (@menufiles) {
140       &scanmenu("$basedir/$item");
141     }
142   }
143
144   if ($file eq 'menunew.pl') {
145     foreach my $item (@menufiles) {
146       &scanmenu("$basedir/$item");
147       print "." if $opt_v;
148     }
149   }
150
151   $file =~ s/\.pl//;
152
153   foreach my $text (keys %$missing) {
154     if ($locale{$text} || $htmllocale{$text}) {
155       unless ($self->{texts}{$text}) {
156         $self->{texts}{$text} = $missing->{$text};
157       }
158     }
159   }
160
161   open FH, ">$file" or die "$! : $file";
162
163   print FH q|#!/usr/bin/perl
164
165 $self->{texts} = {
166 |;
167
168   foreach my $key (sort keys %locale) {
169     my $text    =  $self->{texts}{$key} || $key;
170     $text       =~ s/'/\\'/g;
171     $text       =~ s/\\$/\\\\/;
172
173     my $keytext =  $key;
174     $keytext    =~ s/'/\\'/g;
175     $keytext    =~ s/\\$/\\\\/;
176
177     print FH qq|  '$keytext'|
178       . (' ' x (27 - length($keytext)))
179       . qq| => '$text',\n|;
180   }
181
182   print FH q|};
183
184 $self->{subs} = {
185 |;
186
187   foreach my $key (sort keys %subrt) {
188     my $text =  $key;
189     $text    =~ s/'/\\'/g;
190     $text    =~ s/\\$/\\\\/;
191     print FH qq|  '$text'| . (' ' x (27 - length($text))) . qq| => '$text',\n|;
192   }
193
194   foreach my $key (sort keys %submit) {
195     my $text           =  ($self->{texts}{$key}) ? $self->{texts}{$key} : $key;
196     $text              =~ s/'/\\'/g;
197     $text              =~ s/\\$/\\\\/;
198
199     my $english_sub    =  $key;
200     $english_sub       =~ s/'/\\'/g;
201     $english_sub       =~ s/\\$/\\\\/;
202     $english_sub       = lc $key;
203
204     my $translated_sub =  lc $text;
205     $english_sub       =~ s/( |-|,)/_/g;
206     $translated_sub    =~ s/( |-|,)/_/g;
207     print FH qq|  '$translated_sub'|
208       . (' ' x (27 - length($translated_sub)))
209       . qq| => '$english_sub',\n|;
210   }
211
212   print FH q|};
213
214 1;
215 |;
216
217   close FH;
218
219 }
220
221 #foreach $file (@htmltemplates) {
222 #  converthtmlfile($file);
223 #}
224
225 # now print out all
226
227 open FH, ">all" or die "$! : all";
228
229 print FH q|#!/usr/bin/perl
230
231 # These are all the texts to build the translations files.
232 # The file has the form of 'english text'  => 'foreign text',
233 # you can add the translation in this file or in the 'missing' file
234 # run locales.pl from this directory to rebuild the translation files
235
236 $self->{texts} = {
237 |;
238
239 foreach my $key (sort keys %alllocales) {
240   my $text = $self->{texts}{$key};
241
242   $count++;
243
244   $text =~ s/'/\\'/g;
245   $text =~ s/\\$/\\\\/;
246   $key  =~ s/'/\\'/g;
247   $key  =~ s/\\$/\\\\/;
248
249   unless ($text) {
250     $notext++;
251     push @missing, $key;
252   }
253
254   print FH qq|  '$key'| . (' ' x (27 - length($key))) . qq| => '$text',\n|;
255
256 }
257
258 print FH q|};
259
260 1;
261 |;
262
263 close FH;
264
265 if (@missing) {
266   open FH, ">missing" or die "$! : missing";
267
268   print FH q|#!/usr/bin/perl
269
270 # add the missing texts and run locales.pl to rebuild
271
272 $missing = {
273 |;
274
275   foreach my $text (@missing) {
276     print FH qq|  '$text'| . (' ' x (27 - length($text))) . qq| => '',\n|;
277   }
278
279   print FH q|};
280
281 1;
282 |;
283
284   close FH;
285
286 }
287
288 while (my ($text, $translation) = each %old_texts) {
289   next if ($alllocales{$text});
290
291   push @lost, { 'text' => $text, 'translation' => $translation };
292 }
293
294 if (scalar @lost) {
295   splice @lost, 0, (scalar @lost - 50) if (scalar @lost > 50);
296
297   open FH, ">lost";
298   print FH "#!/usr/bin/perl\n\n" .
299     "# The last 50 texts that have been removed.\n" .
300     "# This file will be auto-generated by locales.pl. Do not edit it.\n\n" .
301     "\@lost = (\n";
302
303   foreach my $entry (@lost) {
304     $entry->{text}        =~ s/\'/\\\'/g;
305     $entry->{translation} =~ s/\'/\\\'/g;
306     print FH "  { 'text' => '$entry->{text}', 'translation' => '$entry->{translation}' },\n";
307   }
308
309   print FH ");\n\n1;\n";
310   close FH;
311 }
312
313 open(FH, "LANGUAGE");
314 my @language = <FH>;
315 close(FH);
316 my $trlanguage = $language[0];
317 chomp $trlanguage;
318
319 if ($opt_c) {
320   search_unused_htmlfiles();
321   search_translated_htmlfiles_wo_master();
322 }
323
324 my $per = sprintf("%.1f", ($count - $notext) / $count * 100);
325 print "\n$trlanguage - ${per}%";
326 print " - $notext/$count missing" if $notext;
327 print "\n";
328
329 exit;
330
331 # eom
332
333 sub extract_text_between_parenthesis {
334   my ($fh, $line) = @_;
335   my ($inside_string, $pos, $text, $quote_next) = (undef, 0, "", 0);
336
337   while (1) {
338     if (length($line) <= $pos) {
339       $line = <$fh>;
340       return ($text, "") unless ($line);
341       $pos = 0;
342     }
343
344     my $cur_char = substr($line, $pos, 1);
345
346     if (!$inside_string) {
347       if ((length($line) >= ($pos + 3)) && (substr($line, $pos, 2)) eq "qq") {
348         $inside_string = substr($line, $pos + 2, 1);
349         $pos += 2;
350
351       } elsif ((length($line) >= ($pos + 2)) &&
352                (substr($line, $pos, 1) eq "q")) {
353         $inside_string = substr($line, $pos + 1, 1);
354         $pos++;
355
356       } elsif (($cur_char eq '"') || ($cur_char eq '\'')) {
357         $inside_string = $cur_char;
358
359       } elsif (($cur_char eq ")") || ($cur_char eq ',')) {
360         return ($text, substr($line, $pos + 1));
361       }
362
363     } else {
364       if ($quote_next) {
365         $text .= $cur_char;
366         $quote_next = 0;
367
368       } elsif ($cur_char eq '\\') {
369         $text .= $cur_char;
370         $quote_next = 1;
371
372       } elsif ($cur_char eq $inside_string) {
373         undef($inside_string);
374
375       } else {
376         $text .= $cur_char;
377
378       }
379     }
380     $pos++;
381   }
382 }
383
384 sub scanfile {
385   my $file = shift;
386   my $dont_include_subs = shift;
387   my $scanned_files = shift;
388
389   # sanitize file
390   $file =~ s=/+=/=g;
391
392   $scanned_files = {} unless ($scanned_files);
393   return if ($scanned_files->{$file});
394   $scanned_files->{$file} = 1;
395
396   if (!defined $cached{$file}) {
397
398     return unless (-f "$file");
399
400     my $fh = new FileHandle;
401     open $fh, "$file" or die "$! : $file";
402
403     my ($is_submit, $line_no, $sub_line_no) = (0, 0, 0);
404
405     while (<$fh>) {
406       $line_no++;
407
408       # is this another file
409       if (/require\s+\W.*\.pl/) {
410         my $newfile = $&;
411         $newfile =~ s/require\s+\W//;
412         $newfile =~ s|bin/mozilla||;
413 #         &scanfile("$bindir/$newfile", 0, $scanned_files);
414          $cached{$file}{scan}{"$bindir/$newfile"} = 1;
415       } elsif (/use\s+SL::(.*?);/) {
416         my $module =  $1;
417         $module    =~ s|::|/|g;
418 #         &scanfile("../../SL/${1}.pm", 1, $scanned_files);
419         $cached{$file}{scannosubs}{"../../SL/${module}.pm"} = 1;
420       }
421
422       # is this a template call?
423       if (/parse_html_template2?\s*\(\s*[\"\']([\w\/]+)\s*[\"\']/) {
424         my $newfile = "$basedir/templates/webpages/$1_master.html";
425         if (/parse_html_template2/) {
426           print "E: " . strip_base($file) . " is still using 'parse_html_template2' for " . strip_base($newfile) . ".\n";
427         }
428         if (-f $newfile) {
429 #           &scanhtmlfile($newfile);
430 #           &converthtmlfile($newfile);
431            $cached{$file}{scanh}{$newfile} = 1;
432           print "." if $opt_v;
433         } elsif ($opt_c) {
434           print "W: missing HTML template: " . strip_base($newfile) . " (referenced from " . strip_base($file) . ")\n";
435         }
436       }
437
438       # is this a sub ?
439       if (/^sub /) {
440         next if ($dont_include_subs);
441         my $subrt = (split / +/)[1];
442 #        $subrt{$subrt} = 1;
443         $cached{$file}{subr}{$subrt} = 1;
444         next;
445       }
446
447       my $rc = 1;
448
449       while ($rc) {
450         if (/Locale/) {
451           unless (/^use /) {
452             my ($null, $country) = split /,/;
453             $country =~ s/^ +[\"\']//;
454             $country =~ s/[\"\'].*//;
455           }
456         }
457
458         my $postmatch = "";
459
460         # is it a submit button before $locale->
461         if (/$submitsearch/) {
462           $postmatch = "$'";
463           if ($` !~ /locale->text/) {
464             $is_submit   = 1;
465             $sub_line_no = $line_no;
466           }
467         }
468
469         my ($found) = /locale->text.*?\(/;
470         my $postmatch = "$'";
471
472         if ($found) {
473           my $string;
474           ($string, $_) = extract_text_between_parenthesis($fh, $postmatch);
475           $postmatch = $_;
476
477           # if there is no $ in the string record it
478           unless (($string =~ /\$\D.*/) || ("" eq $string)) {
479
480             # this guarantees one instance of string
481 #            $locale{$string} = 1;
482             $cached{$file}{locale}{$string} = 1;
483
484             # this one is for all the locales
485 #            $alllocales{$string} = 1;
486             $cached{$file}{all}{$string} = 1;
487
488             # is it a submit button before $locale->
489             if ($is_submit) {
490 #              $submit{$string} = 1;
491               $cached{$file}{submit}{$string} = 1;
492             }
493           }
494         } elsif ($postmatch =~ />/) {
495           $is_submit = 0;
496         }
497
498         # exit loop if there are no more locales on this line
499         ($rc) = ($postmatch =~ /locale->text/);
500
501         if (   ($postmatch =~ />/)
502             || (!$found && ($sub_line_no != $line_no) && />/)) {
503           $is_submit = 0;
504         }
505       }
506     }
507
508     close($fh);
509
510   }
511
512   map { $alllocales{$_} = 1 }   keys %{$cached{$file}{all}};
513   map { $locale{$_} = 1 }       keys %{$cached{$file}{locale}};
514   map { $submit{$_} = 1 }       keys %{$cached{$file}{submit}};
515   map { $subrt{$_} = 1 }        keys %{$cached{$file}{subr}};
516   map { &scanfile($_, 0, $scanned_files) } keys %{$cached{$file}{scan}};
517   map { &scanfile($_, 1, $scanned_files) } keys %{$cached{$file}{scannosubs}};
518   map { &scanhtmlfile($_)  }    keys %{$cached{$file}{scanh}};
519
520   @referenced_html_files{keys %{$cached{$file}{scanh}}} = (1) x scalar keys %{$cached{$file}{scanh}};
521 }
522
523 sub scanmenu {
524   my $file = shift;
525
526   my $fh = new FileHandle;
527   open $fh, "$file" or die "$! : $file";
528
529   my @a = grep m/^\[/, <$fh>;
530   close($fh);
531
532   # strip []
533   grep { s/(\[|\])//g } @a;
534
535   foreach my $item (@a) {
536     my @b = split /--/, $item;
537     foreach my $string (@b) {
538       chomp $string;
539       $locale{$string}     = 1;
540       $alllocales{$string} = 1;
541     }
542   }
543
544 }
545
546 sub scanhtmlfile {
547   local *IN;
548
549   if (!defined $cached{$_[0]}) {
550     my %plugins = ( 'loaded' => { }, 'needed' => { } );
551
552     open(IN, $_[0]) || die $_[0];
553
554     my $copying  = 0;
555     my $issubmit = 0;
556     my $text     = "";
557     while (my $line = <IN>) {
558       chomp($line);
559
560       while ($line =~ m/\[\%[^\w]*use[^\w]+(\w+)[^\w]*?\%\]/gi) {
561         $plugins{loaded}->{$1} = 1;
562       }
563
564       while ($line =~ m/\[\%[^\w]*(\w+)\.\w+\(/g) {
565         my $plugin = $1;
566         $plugins{needed}->{$plugin} = 1 if (first { $_ eq $plugin } qw(HTML LxERP JavaScript MultiColumnIterator));
567       }
568
569       while ($line =~ m/\[\%            # Template-Start-Tag
570                         [\-~#]*         # Whitespace-Unterdrückung
571                         \s*             # Optional beliebig viele Whitespace
572                         [\'\"]          # Anfang des zu übersetzenden Strings
573                         (.*?)           # Der zu übersetzende String
574                         [\'\"]          # Ende des zu übersetzenden Strings
575                         \s*\|\s*        # Pipe-Zeichen mit optionalen Whitespace davor und danach
576                         \$T8            # Filteraufruf
577                         .*?             # Optionale Argumente für den Filter und Whitespaces
578                         [\-~#]*         # Whitespace-Unterdrückung
579                         \%\]            # Template-Ende-Tag
580                        /ix) {
581 #        print "Found filter '$1' in string '$line'\n";
582         $cached{$_[0]}{all}{$1}  = 1;
583         $cached{$_[0]}{html}{$1} = 1;
584         $plugins{needed}->{T8}   = 1;
585         substr $line, $-[0], $+[0] - $-[0], '';
586       }
587
588       while ("" ne $line) {
589         if (!$copying) {
590           if ($line =~ m|<translate>|i) {
591             my $eom = $+[0];
592             if ($` =~ /$submitsearch/) {
593               $issubmit = 1
594             }
595             substr($line, 0, $eom) = "";
596             $copying = 1;
597           } else {
598             $line = "";
599           }
600
601         } else {
602           if ($line =~ m|</translate>|i) {
603             $text .= $`;
604             substr($line, 0, $+[0]) = "";
605             $text =~ s/\s+/ /g;
606
607             $copying = 0;
608             if ($issubmit) {
609   #            $submit{$text} = 1;
610                $cached{$_[0]}{submit}{$text} = 1;
611               $issubmit = 0;
612             }
613   #          $alllocales{$text} = 1;
614              $cached{$_[0]}{all}{$text} = 1;
615   #          $htmllocale{$text} = 1;
616              $cached{$_[0]}{html}{$text} = 1;
617             $text = "";
618
619           } else {
620             $text .= $line;
621             $line = "";
622           }
623         }
624       }
625     }
626
627     close(IN);
628
629     foreach my $plugin (keys %{ $plugins{needed} }) {
630       next if ($plugins{loaded}->{$plugin});
631       print "E: " . strip_base($_[0]) . " requires the Template plugin '$plugin', but is not loaded with '[\% USE $plugin \%]'.\n";
632     }
633
634     &converthtmlfile($_[0]);
635   }
636
637   # copy back into global arrays
638   map { $alllocales{$_} = 1 } keys %{$cached{$_[0]}{all}};
639   map { $htmllocale{$_} = 1 } keys %{$cached{$_[0]}{html}};
640   map { $submit{$_} = 1 }     keys %{$cached{$_[0]}{submit}};
641 }
642
643 sub converthtmlfile {
644   local *IN;
645   local *OUT;
646
647   my $file = shift;
648
649   open(IN, $file) || die;
650
651   my $langcode = (split("/", getcwd()))[-1];
652   $file =~ s/_master.html$/_${langcode}.html/;
653
654   open(OUT, ">$file") || die;
655
656   my $copying = 0;
657   my $text = "";
658   while (my $line = <IN>) {
659     chomp($line);
660     if ("" eq $line) {
661       print(OUT "\n");
662       next;
663     }
664
665     while ("" ne $line) {
666       if (!$copying) {
667         if ($line =~ m|<translate>|i) {
668           print(OUT $`);
669           substr($line, 0, $+[0]) = "";
670           $copying = 1;
671           print(OUT "\n") if ("" eq $line);
672
673         } else {
674           print(OUT "${line}\n");
675           $line = "";
676         }
677
678       } else {
679         if ($line =~ m|</translate>|i) {
680           $text .= $`;
681           substr($line, 0, $+[0]) = "";
682           $text =~ s/\s+/ /g;
683           $copying = 0;
684           $alllocales{$text} = 1;
685           $htmllocale{$text} = 1;
686           print(OUT $self->{"texts"}{$text} || $text);
687           print(OUT "\n") if ("" eq $line);
688           $text = "";
689
690         } else {
691           $text .= $line;
692           $line = "";
693         }
694       }
695     }
696   }
697
698   close(IN);
699   close(OUT);
700 }
701
702 sub search_unused_htmlfiles {
703   my @unscanned_dirs = ('../../templates/webpages');
704
705   while (scalar @unscanned_dirs) {
706     my $dir = shift @unscanned_dirs;
707
708     foreach my $entry (<$dir/*>) {
709       if (-d $entry) {
710         push @unscanned_dirs, $entry;
711
712       } elsif (($entry =~ /_master.html$/) && -f $entry && !$referenced_html_files{$entry}) {
713         print "W: unused HTML template: " . strip_base($entry) . "\n";
714
715       }
716     }
717   }
718 }
719
720 sub search_translated_htmlfiles_wo_master {
721   my @unscanned_dirs = ('../../templates/webpages');
722
723   while (scalar @unscanned_dirs) {
724     my $dir = shift @unscanned_dirs;
725
726     foreach my $entry (<$dir/*>) {
727       if (-d $entry) {
728         push @unscanned_dirs, $entry;
729
730       } elsif (($entry =~ /_[a-z]+\.html$/) && ($entry !~ /_master.html$/) && -f $entry) {
731         my $master =  $entry;
732         $master    =~ s/[a-z]+\.html$/master.html/;
733         if (! -f $master) {
734           print "W: translated HTML template without master: " . strip_base($entry) . "\n";
735         }
736       }
737     }
738   }
739 }
740
741 sub strip_base {
742   my $s =  "$_[0]";             # Create a copy of the string.
743
744   $s    =~ s|^../../||;
745   $s    =~ s|templates/webpages/||;
746
747   return $s;
748 }
749
750 __END__
751
752 =head1 NAME
753
754 locales.pl - Collect strings for translation in Lx-Office
755
756 =head1 SYNOPSIS
757
758 locales.pl [options]
759
760  Options:
761   -n, --no-custom-files  Do not process files whose name contains "_"
762   -c, --check-files      Run extended checks on HTML files
763   -v, --verbose          Be more verbose
764   -h, --help             Show this help
765
766 =head1 OPTIONS
767
768 =over 8
769
770 =item B<-n>, B<--no-custom-files>
771
772 Do not process files whose name contains "_", e.g. "custom_io.pl".
773
774 =item B<-c>, B<--check-files>
775
776 Run extended checks on the usage of templates. This can be used to
777 discover HTML templates that are never used as well as the usage of
778 non-existing HTML templates.
779
780 =item B<-v>, B<--verbose>
781
782 Be more verbose.
783
784 =back
785
786 =head1 DESCRIPTION
787
788 This script collects strings from Perl files, the menu.ini file and
789 HTML templates and puts them into the file "all" for translation.  It
790 also distributes those translations back to the individual files.
791
792 =cut