Translation updates for Koha 3.20 beta
[koha.git] / misc / translator / LangInstaller.pm
1 package LangInstaller;
2
3 # Copyright (C) 2010 Tamil s.a.r.l.
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21
22 use C4::Context;
23 # WARNING: Any other tested YAML library fails to work properly in this
24 # script content
25 use YAML::Syck qw( Dump LoadFile );
26 use Locale::PO;
27 use FindBin qw( $Bin );
28
29 $YAML::Syck::ImplicitTyping = 1;
30
31
32 # Default file header for .po syspref files
33 my $default_pref_po_header = Locale::PO->new(-msgid => '', -msgstr =>
34     "Project-Id-Version: PACKAGE VERSION\\n" .
35     "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\n" .
36     "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n" .
37     "Language-Team: Koha Translate List <koha-translate\@lists.koha-community.org>\\n" .
38     "MIME-Version: 1.0\\n" .
39     "Content-Type: text/plain; charset=UTF-8\\n" .
40     "Content-Transfer-Encoding: 8bit\\n" .
41     "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
42 );
43
44
45 sub set_lang {
46     my ($self, $lang) = @_;
47
48     $self->{lang} = $lang;
49     $self->{po_path_lang} = $self->{context}->config('intrahtdocs') .
50                             "/prog/$lang/modules/admin/preferences";
51 }
52
53
54 sub new {
55     my ($class, $lang, $pref_only, $verbose) = @_;
56
57     my $self                 = { };
58
59     my $context              = C4::Context->new();
60     $self->{context}         = $context;
61     $self->{path_pref_en}    = $context->config('intrahtdocs') .
62                                '/prog/en/modules/admin/preferences';
63     set_lang( $self, $lang ) if $lang;
64     $self->{pref_only}       = $pref_only;
65     $self->{verbose}         = $verbose;
66     $self->{process}         = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
67     $self->{path_po}         = "$Bin/po";
68     $self->{po}              = { '' => $default_pref_po_header };
69     $self->{domain}          = 'messages';
70     $self->{cp}              = `which cp`;
71     $self->{msgmerge}        = `which msgmerge`;
72     $self->{xgettext}        = `which xgettext`;
73     $self->{sed}             = `which sed`;
74     chomp $self->{cp};
75     chomp $self->{msgmerge};
76     chomp $self->{xgettext};
77     chomp $self->{sed};
78
79     # Get all .pref file names
80     opendir my $fh, $self->{path_pref_en};
81     my @pref_files = grep { /.pref/ } readdir($fh);
82     close $fh;
83     $self->{pref_files} = \@pref_files;
84
85     # Get all available language codes
86     opendir $fh, $self->{path_po};
87     my @langs =  map { ($_) =~ /(.*)-pref/ }
88         grep { $_ =~ /.*-pref/ } readdir($fh);
89     closedir $fh;
90     $self->{langs} = \@langs;
91
92     # Map for both interfaces opac/intranet
93     my $opachtdocs = $context->config('opachtdocs');
94     $self->{interface} = [
95         {
96             name   => 'Intranet prog UI',
97             dir    => $context->config('intrahtdocs') . '/prog',
98             suffix => '-staff-prog.po',
99         },
100         {
101             name   => 'Intranet prog help',
102             dir    => $context->config('intrahtdocs') . '/prog/en/modules/help',
103             suffix => '-staff-help.po',
104         },
105     ];
106
107     # OPAC themes
108     opendir my $dh, $context->config('opachtdocs');
109     for my $theme ( grep { not /^\.|lib/ } readdir($dh) ) {
110         push @{$self->{interface}}, {
111             name   => "OPAC $theme",
112             dir    => "$opachtdocs/$theme",
113             suffix => "-opac-$theme.po",
114         };
115     }
116
117     # MARC flavours (hardcoded list)
118     for ( "MARC21", "UNIMARC", "NORMARC" ) {
119         # search for strings on staff & opac marc files
120         my $dirs = $context->config('intrahtdocs') . '/prog';
121         opendir $fh, $context->config('opachtdocs');
122         for ( grep { not /^\.|\.\.|lib$/ } readdir($fh) ) {
123             $dirs .= ' ' . "$opachtdocs/$_";
124         }
125         push @{$self->{interface}}, {
126             name   => "$_",
127             dir    => $dirs,
128             suffix => "-marc-$_.po",
129         };
130     }
131
132     bless $self, $class;
133 }
134
135
136 sub po_filename {
137     my $self = shift;
138
139     my $context    = C4::Context->new;
140     my $trans_path = $Bin . '/po';
141     my $trans_file = "$trans_path/" . $self->{lang} . "-pref.po";
142     return $trans_file;
143 }
144
145
146 sub po_append {
147     my ($self, $id, $comment) = @_;
148     my $po = $self->{po};
149     my $p = $po->{$id};
150     if ( $p ) {
151         $p->comment( $p->comment . "\n" . $comment );
152     }
153     else {
154         $po->{$id} = Locale::PO->new(
155             -comment => $comment,
156             -msgid   => $id,
157             -msgstr  => ''
158         );
159     }
160 }
161
162
163 sub add_prefs {
164     my ($self, $comment, $prefs) = @_;
165
166     for my $pref ( @$prefs ) {
167         my $pref_name = '';
168         for my $element ( @$pref ) {
169             if ( ref( $element) eq 'HASH' ) {
170                 $pref_name = $element->{pref};
171                 last;
172             }
173         }
174         for my $element ( @$pref ) {
175             if ( ref( $element) eq 'HASH' ) {
176                 while ( my ($key, $value) = each(%$element) ) {
177                     next unless $key eq 'choices';
178                     next unless ref($value) eq 'HASH';
179                     for my $ckey ( keys %$value ) {
180                         my $id = $self->{file} . "#$pref_name# " . $value->{$ckey};
181                         $self->po_append( $id, $comment );
182                     }
183                 }
184             }
185             elsif ( $element ) {
186                 $self->po_append( $self->{file} . "#$pref_name# $element", $comment );
187             }
188         }
189     }
190 }
191
192
193 sub get_trans_text {
194     my ($self, $id) = @_;
195
196     my $po = $self->{po}->{$id};
197     return unless $po;
198     return Locale::PO->dequote($po->msgstr);
199 }
200
201
202 sub update_tab_prefs {
203     my ($self, $pref, $prefs) = @_;
204
205     for my $p ( @$prefs ) {
206         my $pref_name = '';
207         next unless $p;
208         for my $element ( @$p ) {
209             if ( ref( $element) eq 'HASH' ) {
210                 $pref_name = $element->{pref};
211                 last;
212             }
213         }
214         for my $i ( 0..@$p-1 ) {
215             my $element = $p->[$i];
216             if ( ref( $element) eq 'HASH' ) {
217                 while ( my ($key, $value) = each(%$element) ) {
218                     next unless $key eq 'choices';
219                     next unless ref($value) eq 'HASH';
220                     for my $ckey ( keys %$value ) {
221                         my $id = $self->{file} . "#$pref_name# " . $value->{$ckey};
222                         my $text = $self->get_trans_text( $id );
223                         $value->{$ckey} = $text if $text;
224                     }
225                 }
226             }
227             elsif ( $element ) {
228                 my $id = $self->{file} . "#$pref_name# $element";
229                 my $text = $self->get_trans_text( $id );
230                 $p->[$i] = $text if $text;
231             }
232         }
233     }
234 }
235
236
237 sub get_po_from_prefs {
238     my $self = shift;
239
240     for my $file ( @{$self->{pref_files}} ) {
241         my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
242         $self->{file} = $file;
243         # Entries for tab titles
244         $self->po_append( $self->{file}, $_ ) for keys %$pref;
245         while ( my ($tab, $tab_content) = each %$pref ) {
246             if ( ref($tab_content) eq 'ARRAY' ) {
247                 $self->add_prefs( $tab, $tab_content );
248                 next;
249             }
250             while ( my ($section, $sysprefs) = each %$tab_content ) {
251                 my $comment = "$tab > $section";
252                 $self->po_append( $self->{file} . " " . $section, $comment );
253                 $self->add_prefs( $comment, $sysprefs );
254             }
255         }
256     }
257 }
258
259
260 sub save_po {
261     my $self = shift;
262
263     # Create file header if it doesn't already exist
264     my $po = $self->{po};
265     $po->{''} ||= $default_pref_po_header;
266
267     # Write .po entries into a file put in Koha standard po directory
268     Locale::PO->save_file_fromhash( $self->po_filename, $po );
269     say "Saved in file: ", $self->po_filename if $self->{verbose};
270 }
271
272
273 sub get_po_merged_with_en {
274     my $self = shift;
275
276     # Get po from current 'en' .pref files
277     $self->get_po_from_prefs();
278     my $po_current = $self->{po};
279
280     # Get po from previous generation
281     my $po_previous = Locale::PO->load_file_ashash( $self->po_filename );
282
283     for my $id ( keys %$po_current ) {
284         my $po =  $po_previous->{Locale::PO->quote($id)};
285         next unless $po;
286         my $text = Locale::PO->dequote( $po->msgstr );
287         $po_current->{$id}->msgstr( $text );
288     }
289 }
290
291
292 sub update_prefs {
293     my $self = shift;
294     print "Update '", $self->{lang},
295           "' preferences .po file from 'en' .pref files\n" if $self->{verbose};
296     $self->get_po_merged_with_en();
297     $self->save_po();
298 }
299
300
301 sub install_prefs {
302     my $self = shift;
303
304     unless ( -r $self->{po_path_lang} ) {
305         print "Koha directories hierarchy for ", $self->{lang}, " must be created first\n";
306         exit;
307     }
308
309     # Get the language .po file merged with last modified 'en' preferences
310     $self->get_po_merged_with_en();
311
312     for my $file ( @{$self->{pref_files}} ) {
313         my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
314         $self->{file} = $file;
315         # First, keys are replaced (tab titles)
316         $pref = do {
317             my %pref = map { 
318                 $self->get_trans_text( $self->{file} ) || $_ => $pref->{$_}
319             } keys %$pref;
320             \%pref;
321         };
322         while ( my ($tab, $tab_content) = each %$pref ) {
323             if ( ref($tab_content) eq 'ARRAY' ) {
324                 $self->update_tab_prefs( $pref, $tab_content );
325                 next;
326             }
327             while ( my ($section, $sysprefs) = each %$tab_content ) {
328                 $self->update_tab_prefs( $pref, $sysprefs );
329             }
330             my $ntab = {};
331             for my $section ( keys %$tab_content ) {
332                 my $id = $self->{file} . " $section";
333                 my $text = $self->get_trans_text($id);
334                 my $nsection = $text ? $text : $section;
335                 $ntab->{$nsection} = $tab_content->{$section};
336             }
337             $pref->{$tab} = $ntab;
338         }
339         my $file_trans = $self->{po_path_lang} . "/$file";
340         print "Write $file\n" if $self->{verbose};
341         open my $fh, ">", $file_trans;
342         print $fh Dump($pref);
343     }
344 }
345
346
347 sub install_tmpl {
348     my ($self, $files) = @_;
349     say "Install templates" if $self->{verbose};
350     for my $trans ( @{$self->{interface}} ) {
351         my @t_dirs = split(" ", $trans->{dir});
352         for my $t_dir ( @t_dirs ) {
353             my @files   = @$files;
354             my @nomarc = ();
355             print
356                 "  Install templates '$trans->{name}'\n",
357                 "    From: $t_dir/en/\n",
358                 "    To  : $t_dir/$self->{lang}\n",
359                 "    With: $self->{path_po}/$self->{lang}$trans->{suffix}\n"
360                 if $self->{verbose};
361
362             my $trans_dir = ( $trans->{name} =~ /help/ )?"$t_dir":"$t_dir/en/";
363             my $lang_dir  = ( $trans->{name} =~ /help/ )?"$t_dir":"$t_dir/$self->{lang}";
364             $lang_dir =~ s|/en/|/$self->{lang}/|;
365             mkdir $lang_dir unless -d $lang_dir;
366             my $excludes = ( $trans->{name} !~ /help/   )?"":"-x 'help'";
367             # if installing MARC po file, only touch corresponding files
368             my $marc     = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":"";            # for MARC translations
369             # if not installing MARC po file, ignore all MARC files
370             @nomarc      = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
371
372             system
373                 "$self->{process} install " .
374                 "-i $trans_dir " .
375                 "-o $lang_dir  ".
376                 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
377                 "$excludes " .
378                 "$marc " .
379                 ( @files   ? ' -f ' . join ' -f ', @files : '') .
380                 ( @nomarc  ? ' -n ' . join ' -n ', @nomarc : '');
381         }
382     }
383 }
384
385
386 sub update_tmpl {
387     my ($self, $files) = @_;
388
389     say "Update templates" if $self->{verbose};
390     for my $trans ( @{$self->{interface}} ) {
391         my @files   = @$files;
392         my @nomarc = ();
393         print
394             "  Update templates '$trans->{name}'\n",
395             "    From: $trans->{dir}/en/\n",
396             "    To  : $self->{path_po}/$self->{lang}$trans->{suffix}\n"
397                 if $self->{verbose};
398
399         my $trans_dir = ( $trans->{name} =~ /help/ )?"$trans->{dir}":join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs
400         # do no process 'help' dirs unless needed
401         my $excludes  = ( $trans->{name} !~ /help/ )?"-x help":"";
402         # if processing MARC po file, only use corresponding files
403         my $marc      = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":"";            # for MARC translations
404         # if not processing MARC po file, ignore all MARC files
405         @nomarc       = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ );      # hardcoded MARC variants
406
407         system
408             "$self->{process} update " .
409             "-i $trans_dir " .
410             "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
411             "$excludes " .
412             "$marc "     .
413             ( @files   ? ' -f ' . join ' -f ', @files : '') .
414             ( @nomarc  ? ' -n ' . join ' -n ', @nomarc : '');
415     }
416 }
417
418
419 sub create_prefs {
420     my $self = shift;
421
422     if ( -e $self->po_filename ) {
423         say "Preferences .po file already exists. Delete it if you want to recreate it.";
424         return;
425     }
426     $self->get_po_from_prefs();
427     $self->save_po();
428 }
429
430
431 sub create_tmpl {
432     my ($self, $files) = @_;
433
434     say "Create templates\n" if $self->{verbose};
435     for my $trans ( @{$self->{interface}} ) {
436         my @files   = @$files;
437         my @nomarc = ();
438         print
439             "  Create templates .po files for '$trans->{name}'\n",
440             "    From: $trans->{dir}/en/\n",
441             "    To  : $self->{path_po}/$self->{lang}$trans->{suffix}\n"
442                 if $self->{verbose};
443
444         my $trans_dir = ( $trans->{name} =~ /help/ )?"$trans->{dir}":join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs
445         my $excludes  = ( $trans->{name} !~ /help/ )?"-x help":"";
446         # if processing MARC po file, only use corresponding files
447         my $marc      = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":"";            # for MARC translations
448         # if not processing MARC po file, ignore all MARC files
449         @nomarc       = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
450
451         system
452             "$self->{process} create " .
453             "-i $trans_dir " .
454             "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
455             "$excludes " .
456             "$marc " .
457             ( @files  ? ' -f ' . join ' -f ', @files   : '') .
458             ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
459     }
460 }
461
462 sub create_messages {
463     my $self = shift;
464
465     print "Create messages ($self->{lang})\n" if $self->{verbose};
466     system
467         "$self->{cp} $self->{domain}.pot " .
468         "$self->{path_po}/$self->{lang}-$self->{domain}.po";
469 }
470
471 sub update_messages {
472     my $self = shift;
473
474     my $pofile = "$self->{path_po}/$self->{lang}-$self->{domain}.po";
475     print "Update messages ($self->{lang})\n" if $self->{verbose};
476     if ( not -f $pofile ) {
477         print "File $pofile does not exist\n" if $self->{verbose};
478         $self->create_messages();
479     }
480     system "$self->{msgmerge} -U $pofile $self->{domain}.pot";
481 }
482
483 sub extract_messages {
484     my $self = shift;
485
486     my $intranetdir = $self->{context}->config('intranetdir');
487     my @files_to_scan;
488     my @directories_to_scan = ('.');
489     my @blacklist = qw(blib koha-tmpl skel tmp t);
490     while (@directories_to_scan) {
491         my $dir = shift @directories_to_scan;
492         opendir DIR, "$intranetdir/$dir" or die "Unable to open $dir: $!";
493         foreach my $entry (readdir DIR) {
494             next if $entry =~ /^\./;
495             my $relentry = "$dir/$entry";
496             $relentry =~ s|^\./||;
497             if (-d "$intranetdir/$relentry" and not grep /^$relentry$/, @blacklist) {
498                 push @directories_to_scan, "$relentry";
499             } elsif (-f "$intranetdir/$relentry" and $relentry =~ /(pl|pm)$/) {
500                 push @files_to_scan, "$relentry";
501             }
502         }
503     }
504
505     my $xgettext_cmd = "$self->{xgettext} -L Perl --from-code=UTF-8 " .
506         "-o $Bin/$self->{domain}.pot -D $intranetdir";
507     $xgettext_cmd .= " $_" foreach (@files_to_scan);
508
509     if (system($xgettext_cmd) != 0) {
510         die "system call failed: $xgettext_cmd";
511     }
512
513     if ( -f "$Bin/$self->{domain}.pot" ) {
514         my $replace_charset_cmd = "$self->{sed} --in-place " .
515             "$Bin/$self->{domain}.pot " .
516             "--expression='s/charset=CHARSET/charset=UTF-8/'";
517         if (system($replace_charset_cmd) != 0) {
518             die "system call failed: $replace_charset_cmd";
519         }
520     } else {
521         print "No messages found\n" if $self->{verbose};
522         return;
523     }
524     return 1;
525 }
526
527 sub remove_pot {
528     my $self = shift;
529
530     unlink "$Bin/$self->{domain}.pot";
531 }
532
533 sub install {
534     my ($self, $files) = @_;
535     return unless $self->{lang};
536     $self->install_tmpl($files) unless $self->{pref_only};
537     $self->install_prefs();
538 }
539
540
541 sub get_all_langs {
542     my $self = shift;
543     opendir( my $dh, $self->{path_po} );
544     my @files = grep { $_ =~ /-pref.po$/ }
545         readdir $dh;
546     @files = map { $_ =~ s/-pref.po$//; $_ } @files;
547 }
548
549
550 sub update {
551     my ($self, $files) = @_;
552     my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
553     my $extract_ok = $self->extract_messages();
554     for my $lang ( @langs ) {
555         $self->set_lang( $lang );
556         $self->update_tmpl($files) unless $self->{pref_only};
557         $self->update_prefs();
558         $self->update_messages() if $extract_ok;
559     }
560     $self->remove_pot() if $extract_ok;
561 }
562
563
564 sub create {
565     my ($self, $files) = @_;
566     return unless $self->{lang};
567     $self->create_tmpl($files) unless $self->{pref_only};
568     $self->create_prefs();
569     if ($self->extract_messages()) {
570         $self->create_messages();
571         $self->remove_pot();
572     }
573 }
574
575
576
577 1;
578
579
580 =head1 NAME
581
582 LangInstaller.pm - Handle templates and preferences translation
583
584 =head1 SYNOPSYS
585
586   my $installer = LangInstaller->new( 'fr-FR' );
587   $installer->create();
588   $installer->update();
589   $installer->install();
590   for my $lang ( @{$installer->{langs} ) {
591     $installer->set_lang( $lan );
592     $installer->install();
593   }
594
595 =head1 METHODS
596
597 =head2 new
598
599 Create a new instance of the installer object. 
600
601 =head2 create
602
603 For the current language, create .po files for templates and preferences based
604 of the english ('en') version.
605
606 =head2 update
607
608 For the current language, update .po files.
609
610 =head2 install
611
612 For the current langage C<$self->{lang}, use .po files to translate the english
613 version of templates and preferences files and copy those files in the
614 appropriate directory.
615
616 =over
617
618 =item translate create F<lang>
619
620 Create 4 kinds of .po files in F<po> subdirectory:
621 (1) one from each theme on opac pages templates,
622 (2) intranet templates and help,
623 (3) preferences, and
624 (4) one for each MARC dialect.
625
626
627 =over
628
629 =item F<lang>-opac-{theme}.po
630
631 Contains extracted text from english (en) OPAC templates found in
632 <KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
633
634 =item F<lang>-staff-prog.po and F<lang>-staff-help.po
635
636 Contains extracted text from english (en) intranet templates found in
637 <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
638
639 =item F<lang>-pref.po
640
641 Contains extracted text from english (en) preferences. They are found in files
642 located in <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/admin/preferences
643 directory.
644
645 =item F<lang>-marc-{MARC}.po
646
647 Contains extracted text from english (en) files from opac and intranet,
648 related with MARC dialects.
649
650 =back
651
652 =item pref-trans update F<lang>
653
654 Update .po files in F<po> directory, named F<lang>-*.po.
655
656 =item pref-trans install F<lang>
657
658 =back
659
660 =cut
661