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