Translation updates for Koha 20.11.11
[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( LoadFile DumpFile );
26 use Locale::PO;
27 use FindBin qw( $Bin );
28 use File::Basename;
29 use File::Path qw( make_path );
30 use File::Copy;
31
32 $YAML::Syck::ImplicitTyping = 1;
33
34 sub set_lang {
35     my ($self, $lang) = @_;
36
37     $self->{lang} = $lang;
38     $self->{po_path_lang} = C4::Context->config('intrahtdocs') .
39                             "/prog/$lang/modules/admin/preferences";
40 }
41
42 sub new {
43     my ($class, $lang, $pref_only, $verbose) = @_;
44
45     my $self                 = { };
46
47     $self->{path_pref_en}    = C4::Context->config('intrahtdocs') .
48                                '/prog/en/modules/admin/preferences';
49     set_lang( $self, $lang ) if $lang;
50     $self->{pref_only}       = $pref_only;
51     $self->{verbose}         = $verbose;
52     $self->{process}         = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
53     $self->{path_po}         = "$Bin/po";
54     $self->{po}              = {};
55     $self->{domain}          = 'Koha';
56     $self->{msgfmt}          = `which msgfmt`;
57     $self->{po2json}         = "$Bin/po2json";
58     $self->{gzip}            = `which gzip`;
59     $self->{gunzip}          = `which gunzip`;
60     chomp $self->{msgfmt};
61     chomp $self->{gzip};
62     chomp $self->{gunzip};
63
64     # Get all .pref file names
65     opendir my $fh, $self->{path_pref_en};
66     my @pref_files = grep { /\.pref$/ } readdir($fh);
67     close $fh;
68     $self->{pref_files} = \@pref_files;
69
70     # Get all available language codes
71     opendir $fh, $self->{path_po};
72     my @langs =  map { ($_) =~ /(.*)-pref/ }
73         grep { $_ =~ /.*-pref/ } readdir($fh);
74     closedir $fh;
75     $self->{langs} = \@langs;
76
77     # Map for both interfaces opac/intranet
78     my $opachtdocs = C4::Context->config('opachtdocs');
79     $self->{interface} = [
80         {
81             name   => 'Intranet prog UI',
82             dir    => C4::Context->config('intrahtdocs') . '/prog',
83             suffix => '-staff-prog.po',
84         },
85     ];
86
87     # OPAC themes
88     opendir my $dh, C4::Context->config('opachtdocs');
89     for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
90         push @{$self->{interface}}, {
91             name   => "OPAC $theme",
92             dir    => "$opachtdocs/$theme",
93             suffix => "-opac-$theme.po",
94         };
95     }
96
97     # MARC flavours (hardcoded list)
98     for ( "MARC21", "UNIMARC", "NORMARC" ) {
99         # search for strings on staff & opac marc files
100         my $dirs = C4::Context->config('intrahtdocs') . '/prog';
101         opendir $fh, C4::Context->config('opachtdocs');
102         for ( grep { not /^\.|\.\.|lib$|xslt/ } readdir($fh) ) {
103             $dirs .= ' ' . "$opachtdocs/$_";
104         }
105         push @{$self->{interface}}, {
106             name   => "$_",
107             dir    => $dirs,
108             suffix => "-marc-$_.po",
109         };
110     }
111
112     # EN YAML installer files
113     push @{$self->{installer}}, {
114         name   => "YAML installer files",
115         dirs   => [ 'installer/data/mysql/en/mandatory',
116                     'installer/data/mysql/en/optional'],
117         suffix => "-installer.po",
118     };
119
120     # EN MARC21 YAML installer files
121     push @{$self->{installer}}, {
122         name   => "MARC21 YAML installer files",
123         dirs   => [ 'installer/data/mysql/en/marcflavour/marc21/mandatory',
124                     'installer/data/mysql/en/marcflavour/marc21/optional'],
125         suffix => "-installer-MARC21.po",
126     };
127
128     # EN UNIMARC YAML installer files
129     push @{$self->{installer}}, {
130         name   => "UNIMARC YAML installer files",
131         dirs   => [ 'installer/data/mysql/en/marcflavour/unimarc/mandatory', ],
132         suffix => "-installer-UNIMARC.po",
133     };
134
135     bless $self, $class;
136 }
137
138 sub po_filename {
139     my $self   = shift;
140     my $suffix = shift;
141
142     my $trans_path = $Bin . '/po';
143     my $trans_file = "$trans_path/" . $self->{lang} . $suffix;
144     return $trans_file;
145 }
146
147 sub get_trans_text {
148     my ($self, $msgid, $default) = @_;
149
150     my $po = $self->{po}->{Locale::PO->quote($msgid)};
151     if ($po) {
152         my $msgstr = Locale::PO->dequote($po->msgstr);
153         if ($msgstr and length($msgstr) > 0) {
154             return $msgstr;
155         }
156     }
157
158     return $default;
159 }
160
161 sub get_translated_tab_content {
162     my ($self, $file, $tab_content) = @_;
163
164     if ( ref($tab_content) eq 'ARRAY' ) {
165         return $self->get_translated_prefs($file, $tab_content);
166     }
167
168     my $translated_tab_content = {
169         map {
170             my $section = $_;
171             my $sysprefs = $tab_content->{$section};
172             my $msgid = sprintf('%s %s', $file, $section);
173
174             $self->get_trans_text($msgid, $section) => $self->get_translated_prefs($file, $sysprefs);
175         } keys %$tab_content
176     };
177
178     return $translated_tab_content;
179 }
180
181 sub get_translated_prefs {
182     my ($self, $file, $sysprefs) = @_;
183
184     my $translated_prefs = [
185         map {
186             my ($pref_elt) = grep { ref($_) eq 'HASH' && exists $_->{pref} } @$_;
187             my $pref_name = $pref_elt ? $pref_elt->{pref} : '';
188
189             my $translated_syspref = [
190                 map {
191                     $self->get_translated_pref($file, $pref_name, $_);
192                 } @$_
193             ];
194
195             $translated_syspref;
196         } @$sysprefs
197     ];
198
199     return $translated_prefs;
200 }
201
202 sub get_translated_pref {
203     my ($self, $file, $pref_name, $syspref) = @_;
204
205     unless (ref($syspref)) {
206         $syspref //= '';
207         my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $syspref);
208         return $self->get_trans_text($msgid, $syspref);
209     }
210
211     my $translated_pref = {
212         map {
213             my $key = $_;
214             my $value = $syspref->{$key};
215
216             my $translated_value = $value;
217             if (($key eq 'choices' || $key eq 'multiple') && ref($value) eq 'HASH') {
218                 $translated_value = {
219                     map {
220                         my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $value->{$_});
221                         $_ => $self->get_trans_text($msgid, $value->{$_})
222                     } keys %$value
223                 }
224             }
225
226             $key => $translated_value
227         } keys %$syspref
228     };
229
230     return $translated_pref;
231 }
232
233 sub install_prefs {
234     my $self = shift;
235
236     unless ( -r $self->{po_path_lang} ) {
237         print "Koha directories hierarchy for ", $self->{lang}, " must be created first\n";
238         exit;
239     }
240
241     $self->{po} = Locale::PO->load_file_ashash($self->po_filename("-pref.po"), 'utf8');
242
243     for my $file ( @{$self->{pref_files}} ) {
244         my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
245
246         my $translated_pref = {
247             map {
248                 my $tab = $_;
249                 my $tab_content = $pref->{$tab};
250
251                 $self->get_trans_text($file, $tab) => $self->get_translated_tab_content($file, $tab_content);
252             } keys %$pref
253         };
254
255
256         my $file_trans = $self->{po_path_lang} . "/$file";
257         print "Write $file\n" if $self->{verbose};
258         DumpFile($file_trans, $translated_pref);
259     }
260 }
261
262
263 sub install_tmpl {
264     my ($self, $files) = @_;
265     say "Install templates" if $self->{verbose};
266     for my $trans ( @{$self->{interface}} ) {
267         my @t_dirs = split(" ", $trans->{dir});
268         for my $t_dir ( @t_dirs ) {
269             my @files   = @$files;
270             my @nomarc = ();
271             print
272                 "  Install templates '$trans->{name}'\n",
273                 "    From: $t_dir/en/\n",
274                 "    To  : $t_dir/$self->{lang}\n",
275                 "    With: $self->{path_po}/$self->{lang}$trans->{suffix}\n"
276                 if $self->{verbose};
277
278             my $trans_dir = "$t_dir/en/";
279             my $lang_dir  = "$t_dir/$self->{lang}";
280             $lang_dir =~ s|/en/|/$self->{lang}/|;
281             mkdir $lang_dir unless -d $lang_dir;
282             # if installing MARC po file, only touch corresponding files
283             my $marc     = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":"";            # for MARC translations
284             # if not installing MARC po file, ignore all MARC files
285             @nomarc      = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
286
287             system
288                 "$self->{process} install " .
289                 "-i $trans_dir " .
290                 "-o $lang_dir  ".
291                 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
292                 "$marc " .
293                 ( @files   ? ' -f ' . join ' -f ', @files : '') .
294                 ( @nomarc  ? ' -n ' . join ' -n ', @nomarc : '');
295         }
296     }
297 }
298
299 sub translate_yaml {
300     my $self   = shift;
301     my $target = shift;
302     my $srcyml = shift;
303
304     my $po_file = $self->po_filename( $target->{suffix} );
305     return $srcyml unless ( -e $po_file );
306
307     my $po_ref  = Locale::PO->load_file_ashash( $po_file );
308
309     my $dstyml   = LoadFile( $srcyml );
310
311     # translate fields in table rows
312     my @tables = @{ $dstyml->{'tables'} };
313     for my $table ( @tables ) {                                                         # each table
314         my $table_name = ( keys %$table )[0];
315         my @translatable = @{ $table->{$table_name}->{translatable} };
316         my @rows = @{ $table->{$table_name}->{rows} };
317         my @multiline = @{ $table->{$table_name}->{'multiline'} };                      # to check multiline values
318         for my $row ( @rows ) {                                                         # each row
319             for my $field ( @translatable ) {                                           # each translatable field
320                 if ( @multiline and grep { $_ eq $field } @multiline ) {                # multiline fields, only notices ATM
321                     foreach my $line ( @{$row->{$field}} ) {
322                         next if ( $line =~ /^(\s*<.*?>\s*$|^\s*\[.*?\]\s*|\s*)$/ );     # discard pure html, TT, empty
323                         my @ttvar;
324                         while ( $line =~ s/(<<.*?>>|\[\%.*?\%\]|<.*?>)/\%s/ ) {         # put placeholders, save matches
325                             my $var = $1;
326                             push @ttvar, $var;
327                         }
328
329                         if ( $line =~ /^(\s|%s|-|[[:punct:]]|\(|\))*$/ ) {              # ignore non strings
330                             while ( @ttvar ) {                                          # restore placeholders
331                                 my $var = shift @ttvar;
332                                 $line =~ s/\%s/$var/;
333                             }
334                             next;
335                         } else {
336                             my $po = $po_ref->{"\"$line\""};                            # quoted key
337                             if ( $po  and not defined( $po->fuzzy() )                   # not fuzzy
338                                       and length( $po->msgid() ) > 2                    # not empty msgid
339                                       and length( $po->msgstr() ) > 2 ) {               # not empty msgstr
340                                 $line = $po->dequote( $po->msgstr() );
341                             }
342                             while ( @ttvar ) {                                          # restore placeholders
343                                 my $var = shift @ttvar;
344                                 $line =~ s/\%s/$var/;
345                             }
346                         }
347                     }
348                 } else {
349                     next unless defined $row->{$field};                                 # next if null value
350                     my $po = $po_ref->{"\"$row->{$field}\""};                           # quoted key
351                     if ( $po  and not defined( $po->fuzzy() )                           # not fuzzy
352                               and length( $po->msgid() ) > 2                            # not empty msgid
353                               and length( $po->msgstr() ) > 2 ) {                       # not empty msgstr
354                         $row->{$field} = $po->dequote( $po->msgstr() );
355                     }
356                 }
357             }
358         }
359     }
360
361     # translate descriptions
362     for my $description ( @{ $dstyml->{'description'} } ) {
363         my $po = $po_ref->{"\"$description\""};
364         if ( $po  and not defined( $po->fuzzy() )
365                   and length( $po->msgid() ) > 2
366                   and length( $po->msgstr() ) > 2 ) {
367             $description = $po->dequote( $po->msgstr() );
368         }
369     }
370
371     return $dstyml;
372 }
373
374 sub install_installer {
375     my $self = shift;
376     return unless ( $self->{installer} );
377
378     my $intradir  = C4::Context->config('intranetdir');
379     my $db_scheme = C4::Context->config('db_scheme');
380     my $langdir  = "$intradir/installer/data/$db_scheme/$self->{lang}";
381     if ( -d $langdir ) {
382         say "$self->{lang} installer dir $langdir already exists.\nDelete it if you want to recreate it." if $self->{verbose};
383         return;
384     }
385
386     say "Install installer files\n" if $self->{verbose};
387
388     for my $target ( @{ $self->{installer} } ) {
389         return unless ( -e $self->po_filename( $target->{suffix} ) );
390         for my $dir ( @{ $target->{dirs} } ) {
391             ( my $tdir = "$dir" ) =~ s|/en/|/$self->{lang}/|;
392             make_path("$intradir/$tdir");
393
394             opendir( my $dh, "$intradir/$dir" ) or die ("Can't open $intradir/$dir");
395             my @files = grep { ! /^\.+$/ } readdir($dh);
396             close($dh);
397
398             for my $file ( @files ) {
399                 if ( $file =~ /yml$/ ) {
400                     my $translated_yaml = translate_yaml( $self, $target, "$intradir/$dir/$file" );
401                     open(my $fh, ">:encoding(UTF-8)", "$intradir/$tdir/$file");
402                     DumpFile( $fh, $translated_yaml );
403                     close($fh);
404                 } else {
405                     File::Copy::copy( "$intradir/$dir/$file", "$intradir/$tdir/$file" );
406                 }
407             }
408         }
409     }
410 }
411
412 sub locale_name {
413     my $self = shift;
414
415     my ($language, $region, $country) = split /-/, $self->{lang};
416     $country //= $region;
417     my $locale = $language;
418     if ($country && length($country) == 2) {
419         $locale .= '_' . $country;
420     }
421
422     return $locale;
423 }
424
425 sub install_messages {
426     my ($self) = @_;
427
428     my $locale = $self->locale_name();
429     my $modir = "$self->{path_po}/$locale/LC_MESSAGES";
430     my $pofile = "$self->{path_po}/$self->{lang}-messages.po";
431     my $mofile = "$modir/$self->{domain}.mo";
432     my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po";
433
434     unless ( -f $pofile && -f $js_pofile ) {
435         die "PO files for language '$self->{lang}' do not exist";
436     }
437
438     say "Install messages ($locale)" if $self->{verbose};
439     make_path($modir);
440     system "$self->{msgfmt} -o $mofile $pofile";
441
442     my $js_locale_data = 'var json_locale_data = {"Koha":' . `$self->{po2json} $js_pofile` . '};';
443     my $progdir = C4::Context->config('intrahtdocs') . '/prog';
444     mkdir "$progdir/$self->{lang}/js";
445     open my $fh, '>', "$progdir/$self->{lang}/js/locale_data.js";
446     print $fh $js_locale_data;
447     close $fh;
448
449     my $opachtdocs = C4::Context->config('opachtdocs');
450     opendir(my $dh, $opachtdocs);
451     for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
452         mkdir "$opachtdocs/$theme/$self->{lang}/js";
453         open my $fh, '>', "$opachtdocs/$theme/$self->{lang}/js/locale_data.js";
454         print $fh $js_locale_data;
455         close $fh;
456     }
457 }
458
459 sub compress {
460     my ($self, $files) = @_;
461     my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
462     for my $lang ( @langs ) {
463         $self->set_lang( $lang );
464         opendir( my $dh, $self->{path_po} );
465         my @files = grep { $_ =~ /^$self->{lang}.*po$/ } readdir $dh;
466         foreach my $file ( @files ) {
467             say "Compress file $file" if $self->{verbose};
468             system "$self->{gzip} -9 $self->{path_po}/$file";
469         }
470     }
471 }
472
473 sub uncompress {
474     my ($self, $files) = @_;
475     my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
476     for my $lang ( @langs ) {
477         opendir( my $dh, $self->{path_po} );
478         $self->set_lang( $lang );
479         my @files = grep { $_ =~ /^$self->{lang}.*po.gz$/ } readdir $dh;
480         foreach my $file ( @files ) {
481             say "Uncompress file $file" if $self->{verbose};
482             system "$self->{gunzip} $self->{path_po}/$file";
483         }
484     }
485 }
486
487 sub install {
488     my ($self, $files) = @_;
489     return unless $self->{lang};
490     $self->uncompress();
491
492     if ($self->{pref_only}) {
493         $self->install_prefs();
494     } else {
495         $self->install_tmpl($files);
496         $self->install_prefs();
497         $self->install_messages();
498         $self->install_installer();
499     }
500 }
501
502
503 sub get_all_langs {
504     my $self = shift;
505     opendir( my $dh, $self->{path_po} );
506     my @files = grep { $_ =~ /-pref.(po|po.gz)$/ }
507         readdir $dh;
508     @files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files;
509 }
510
511 1;
512
513
514 =head1 NAME
515
516 LangInstaller.pm - Handle templates and preferences translation
517
518 =head1 SYNOPSYS
519
520   my $installer = LangInstaller->new( 'fr-FR' );
521   $installer->create();
522   $installer->update();
523   $installer->install();
524   for my $lang ( @{$installer->{langs} ) {
525     $installer->set_lang( $lan );
526     $installer->install();
527   }
528
529 =head1 METHODS
530
531 =head2 new
532
533 Create a new instance of the installer object. 
534
535 =head2 create
536
537 For the current language, create .po files for templates and preferences based
538 of the english ('en') version.
539
540 =head2 update
541
542 For the current language, update .po files.
543
544 =head2 install
545
546 For the current langage C<$self->{lang}, use .po files to translate the english
547 version of templates and preferences files and copy those files in the
548 appropriate directory.
549
550 =over
551
552 =item translate create F<lang>
553
554 Create 4 kinds of .po files in F<po> subdirectory:
555 (1) one from each theme on opac pages templates,
556 (2) intranet templates,
557 (3) preferences, and
558 (4) one for each MARC dialect.
559
560
561 =over
562
563 =item F<lang>-opac-{theme}.po
564
565 Contains extracted text from english (en) OPAC templates found in
566 <KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
567
568 =item F<lang>-staff-prog.po
569
570 Contains extracted text from english (en) intranet templates found in
571 <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
572
573 =item F<lang>-pref.po
574
575 Contains extracted text from english (en) preferences. They are found in files
576 located in <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/admin/preferences
577 directory.
578
579 =item F<lang>-marc-{MARC}.po
580
581 Contains extracted text from english (en) files from opac and intranet,
582 related with MARC dialects.
583
584 =back
585
586 =item pref-trans update F<lang>
587
588 Update .po files in F<po> directory, named F<lang>-*.po.
589
590 =item pref-trans install F<lang>
591
592 =back
593
594 =cut
595