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