Bug 21895: Fix path to POT file
[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 use File::Basename;
29 use File::Find;
30 use File::Path qw( make_path );
31 use File::Slurp;
32 use File::Spec;
33 use File::Temp qw( tempdir );
34 use Template::Parser;
35 use PPI;
36
37 $YAML::Syck::ImplicitTyping = 1;
38
39
40 # Default file header for .po syspref files
41 my $default_pref_po_header = Locale::PO->new(-msgid => '', -msgstr =>
42     "Project-Id-Version: PACKAGE VERSION\\n" .
43     "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\n" .
44     "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n" .
45     "Language-Team: Koha Translate List <koha-translate\@lists.koha-community.org>\\n" .
46     "MIME-Version: 1.0\\n" .
47     "Content-Type: text/plain; charset=UTF-8\\n" .
48     "Content-Transfer-Encoding: 8bit\\n" .
49     "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
50 );
51
52
53 sub set_lang {
54     my ($self, $lang) = @_;
55
56     $self->{lang} = $lang;
57     $self->{po_path_lang} = $self->{context}->config('intrahtdocs') .
58                             "/prog/$lang/modules/admin/preferences";
59 }
60
61
62 sub new {
63     my ($class, $lang, $pref_only, $verbose) = @_;
64
65     my $self                 = { };
66
67     my $context              = C4::Context->new();
68     $self->{context}         = $context;
69     $self->{path_pref_en}    = $context->config('intrahtdocs') .
70                                '/prog/en/modules/admin/preferences';
71     set_lang( $self, $lang ) if $lang;
72     $self->{pref_only}       = $pref_only;
73     $self->{verbose}         = $verbose;
74     $self->{process}         = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
75     $self->{path_po}         = "$Bin/po";
76     $self->{po}              = { '' => $default_pref_po_header };
77     $self->{domain}          = 'Koha';
78     $self->{cp}              = `which cp`;
79     $self->{msgmerge}        = `which msgmerge`;
80     $self->{msgfmt}          = `which msgfmt`;
81     $self->{msginit}         = `which msginit`;
82     $self->{xgettext}        = `which xgettext`;
83     $self->{sed}             = `which sed`;
84     chomp $self->{cp};
85     chomp $self->{msgmerge};
86     chomp $self->{msgfmt};
87     chomp $self->{msginit};
88     chomp $self->{xgettext};
89     chomp $self->{sed};
90
91     unless ($self->{xgettext}) {
92         die "Missing 'xgettext' executable. Have you installed the gettext package?\n";
93     }
94
95     # Get all .pref file names
96     opendir my $fh, $self->{path_pref_en};
97     my @pref_files = grep { /\.pref$/ } readdir($fh);
98     close $fh;
99     $self->{pref_files} = \@pref_files;
100
101     # Get all available language codes
102     opendir $fh, $self->{path_po};
103     my @langs =  map { ($_) =~ /(.*)-pref/ }
104         grep { $_ =~ /.*-pref/ } readdir($fh);
105     closedir $fh;
106     $self->{langs} = \@langs;
107
108     # Map for both interfaces opac/intranet
109     my $opachtdocs = $context->config('opachtdocs');
110     $self->{interface} = [
111         {
112             name   => 'Intranet prog UI',
113             dir    => $context->config('intrahtdocs') . '/prog',
114             suffix => '-staff-prog.po',
115         },
116     ];
117
118     # OPAC themes
119     opendir my $dh, $context->config('opachtdocs');
120     for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
121         push @{$self->{interface}}, {
122             name   => "OPAC $theme",
123             dir    => "$opachtdocs/$theme",
124             suffix => "-opac-$theme.po",
125         };
126     }
127
128     # MARC flavours (hardcoded list)
129     for ( "MARC21", "UNIMARC", "NORMARC" ) {
130         # search for strings on staff & opac marc files
131         my $dirs = $context->config('intrahtdocs') . '/prog';
132         opendir $fh, $context->config('opachtdocs');
133         for ( grep { not /^\.|\.\.|lib$|xslt/ } readdir($fh) ) {
134             $dirs .= ' ' . "$opachtdocs/$_";
135         }
136         push @{$self->{interface}}, {
137             name   => "$_",
138             dir    => $dirs,
139             suffix => "-marc-$_.po",
140         };
141     }
142
143     bless $self, $class;
144 }
145
146
147 sub po_filename {
148     my $self = shift;
149
150     my $context    = C4::Context->new;
151     my $trans_path = $Bin . '/po';
152     my $trans_file = "$trans_path/" . $self->{lang} . "-pref.po";
153     return $trans_file;
154 }
155
156
157 sub po_append {
158     my ($self, $id, $comment) = @_;
159     my $po = $self->{po};
160     my $p = $po->{$id};
161     if ( $p ) {
162         $p->comment( $p->comment . "\n" . $comment );
163     }
164     else {
165         $po->{$id} = Locale::PO->new(
166             -comment => $comment,
167             -msgid   => $id,
168             -msgstr  => ''
169         );
170     }
171 }
172
173
174 sub add_prefs {
175     my ($self, $comment, $prefs) = @_;
176
177     for my $pref ( @$prefs ) {
178         my $pref_name = '';
179         for my $element ( @$pref ) {
180             if ( ref( $element) eq 'HASH' ) {
181                 $pref_name = $element->{pref};
182                 last;
183             }
184         }
185         for my $element ( @$pref ) {
186             if ( ref( $element) eq 'HASH' ) {
187                 while ( my ($key, $value) = each(%$element) ) {
188                     next unless $key eq 'choices';
189                     next unless ref($value) eq 'HASH';
190                     for my $ckey ( keys %$value ) {
191                         my $id = $self->{file} . "#$pref_name# " . $value->{$ckey};
192                         $self->po_append( $id, $comment );
193                     }
194                 }
195             }
196             elsif ( $element ) {
197                 $self->po_append( $self->{file} . "#$pref_name# $element", $comment );
198             }
199         }
200     }
201 }
202
203
204 sub get_trans_text {
205     my ($self, $id) = @_;
206
207     my $po = $self->{po}->{$id};
208     return unless $po;
209     return Locale::PO->dequote($po->msgstr);
210 }
211
212
213 sub update_tab_prefs {
214     my ($self, $pref, $prefs) = @_;
215
216     for my $p ( @$prefs ) {
217         my $pref_name = '';
218         next unless $p;
219         for my $element ( @$p ) {
220             if ( ref( $element) eq 'HASH' ) {
221                 $pref_name = $element->{pref};
222                 last;
223             }
224         }
225         for my $i ( 0..@$p-1 ) {
226             my $element = $p->[$i];
227             if ( ref( $element) eq 'HASH' ) {
228                 while ( my ($key, $value) = each(%$element) ) {
229                     next unless $key eq 'choices';
230                     next unless ref($value) eq 'HASH';
231                     for my $ckey ( keys %$value ) {
232                         my $id = $self->{file} . "#$pref_name# " . $value->{$ckey};
233                         my $text = $self->get_trans_text( $id );
234                         $value->{$ckey} = $text if $text;
235                     }
236                 }
237             }
238             elsif ( $element ) {
239                 my $id = $self->{file} . "#$pref_name# $element";
240                 my $text = $self->get_trans_text( $id );
241                 $p->[$i] = $text if $text;
242             }
243         }
244     }
245 }
246
247
248 sub get_po_from_prefs {
249     my $self = shift;
250
251     for my $file ( @{$self->{pref_files}} ) {
252         my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
253         $self->{file} = $file;
254         # Entries for tab titles
255         $self->po_append( $self->{file}, $_ ) for keys %$pref;
256         while ( my ($tab, $tab_content) = each %$pref ) {
257             if ( ref($tab_content) eq 'ARRAY' ) {
258                 $self->add_prefs( $tab, $tab_content );
259                 next;
260             }
261             while ( my ($section, $sysprefs) = each %$tab_content ) {
262                 my $comment = "$tab > $section";
263                 $self->po_append( $self->{file} . " " . $section, $comment );
264                 $self->add_prefs( $comment, $sysprefs );
265             }
266         }
267     }
268 }
269
270
271 sub save_po {
272     my $self = shift;
273
274     # Create file header if it doesn't already exist
275     my $po = $self->{po};
276     $po->{''} ||= $default_pref_po_header;
277
278     # Write .po entries into a file put in Koha standard po directory
279     Locale::PO->save_file_fromhash( $self->po_filename, $po );
280     say "Saved in file: ", $self->po_filename if $self->{verbose};
281 }
282
283
284 sub get_po_merged_with_en {
285     my $self = shift;
286
287     # Get po from current 'en' .pref files
288     $self->get_po_from_prefs();
289     my $po_current = $self->{po};
290
291     # Get po from previous generation
292     my $po_previous = Locale::PO->load_file_ashash( $self->po_filename );
293
294     for my $id ( keys %$po_current ) {
295         my $po =  $po_previous->{Locale::PO->quote($id)};
296         next unless $po;
297         my $text = Locale::PO->dequote( $po->msgstr );
298         $po_current->{$id}->msgstr( $text );
299     }
300 }
301
302
303 sub update_prefs {
304     my $self = shift;
305     print "Update '", $self->{lang},
306           "' preferences .po file from 'en' .pref files\n" if $self->{verbose};
307     $self->get_po_merged_with_en();
308     $self->save_po();
309 }
310
311
312 sub install_prefs {
313     my $self = shift;
314
315     unless ( -r $self->{po_path_lang} ) {
316         print "Koha directories hierarchy for ", $self->{lang}, " must be created first\n";
317         exit;
318     }
319
320     # Get the language .po file merged with last modified 'en' preferences
321     $self->get_po_merged_with_en();
322
323     for my $file ( @{$self->{pref_files}} ) {
324         my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
325         $self->{file} = $file;
326         # First, keys are replaced (tab titles)
327         $pref = do {
328             my %pref = map { 
329                 $self->get_trans_text( $self->{file} ) || $_ => $pref->{$_}
330             } keys %$pref;
331             \%pref;
332         };
333         while ( my ($tab, $tab_content) = each %$pref ) {
334             if ( ref($tab_content) eq 'ARRAY' ) {
335                 $self->update_tab_prefs( $pref, $tab_content );
336                 next;
337             }
338             while ( my ($section, $sysprefs) = each %$tab_content ) {
339                 $self->update_tab_prefs( $pref, $sysprefs );
340             }
341             my $ntab = {};
342             for my $section ( keys %$tab_content ) {
343                 my $id = $self->{file} . " $section";
344                 my $text = $self->get_trans_text($id);
345                 my $nsection = $text ? $text : $section;
346                 if( exists $ntab->{$nsection} ) {
347                     # When translations collide (see BZ 18634)
348                     push @{$ntab->{$nsection}}, @{$tab_content->{$section}};
349                 } else {
350                     $ntab->{$nsection} = $tab_content->{$section};
351                 }
352             }
353             $pref->{$tab} = $ntab;
354         }
355         my $file_trans = $self->{po_path_lang} . "/$file";
356         print "Write $file\n" if $self->{verbose};
357         open my $fh, ">", $file_trans;
358         print $fh Dump($pref);
359     }
360 }
361
362
363 sub install_tmpl {
364     my ($self, $files) = @_;
365     say "Install templates" if $self->{verbose};
366     for my $trans ( @{$self->{interface}} ) {
367         my @t_dirs = split(" ", $trans->{dir});
368         for my $t_dir ( @t_dirs ) {
369             my @files   = @$files;
370             my @nomarc = ();
371             print
372                 "  Install templates '$trans->{name}'\n",
373                 "    From: $t_dir/en/\n",
374                 "    To  : $t_dir/$self->{lang}\n",
375                 "    With: $self->{path_po}/$self->{lang}$trans->{suffix}\n"
376                 if $self->{verbose};
377
378             my $trans_dir = "$t_dir/en/";
379             my $lang_dir  = "$t_dir/$self->{lang}";
380             $lang_dir =~ s|/en/|/$self->{lang}/|;
381             mkdir $lang_dir unless -d $lang_dir;
382             # if installing MARC po file, only touch corresponding files
383             my $marc     = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":"";            # for MARC translations
384             # if not installing MARC po file, ignore all MARC files
385             @nomarc      = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
386
387             system
388                 "$self->{process} install " .
389                 "-i $trans_dir " .
390                 "-o $lang_dir  ".
391                 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
392                 "$marc " .
393                 ( @files   ? ' -f ' . join ' -f ', @files : '') .
394                 ( @nomarc  ? ' -n ' . join ' -n ', @nomarc : '');
395         }
396     }
397 }
398
399
400 sub update_tmpl {
401     my ($self, $files) = @_;
402
403     say "Update templates" if $self->{verbose};
404     for my $trans ( @{$self->{interface}} ) {
405         my @files   = @$files;
406         my @nomarc = ();
407         print
408             "  Update templates '$trans->{name}'\n",
409             "    From: $trans->{dir}/en/\n",
410             "    To  : $self->{path_po}/$self->{lang}$trans->{suffix}\n"
411                 if $self->{verbose};
412
413         my $trans_dir = join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs
414         # if processing MARC po file, only use corresponding files
415         my $marc      = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":"";            # for MARC translations
416         # if not processing MARC po file, ignore all MARC files
417         @nomarc       = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ );      # hardcoded MARC variants
418
419         system
420             "$self->{process} update " .
421             "-i $trans_dir " .
422             "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
423             "$marc "     .
424             ( @files   ? ' -f ' . join ' -f ', @files : '') .
425             ( @nomarc  ? ' -n ' . join ' -n ', @nomarc : '');
426     }
427 }
428
429
430 sub create_prefs {
431     my $self = shift;
432
433     if ( -e $self->po_filename ) {
434         say "Preferences .po file already exists. Delete it if you want to recreate it.";
435         return;
436     }
437     $self->get_po_from_prefs();
438     $self->save_po();
439 }
440
441
442 sub create_tmpl {
443     my ($self, $files) = @_;
444
445     say "Create templates\n" if $self->{verbose};
446     for my $trans ( @{$self->{interface}} ) {
447         my @files   = @$files;
448         my @nomarc = ();
449         print
450             "  Create templates .po files for '$trans->{name}'\n",
451             "    From: $trans->{dir}/en/\n",
452             "    To  : $self->{path_po}/$self->{lang}$trans->{suffix}\n"
453                 if $self->{verbose};
454
455         my $trans_dir = join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs
456         # if processing MARC po file, only use corresponding files
457         my $marc      = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":"";            # for MARC translations
458         # if not processing MARC po file, ignore all MARC files
459         @nomarc       = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
460
461         system
462             "$self->{process} create " .
463             "-i $trans_dir " .
464             "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
465             "$marc " .
466             ( @files  ? ' -f ' . join ' -f ', @files   : '') .
467             ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
468     }
469 }
470
471 sub locale_name {
472     my $self = shift;
473
474     my ($language, $region, $country) = split /-/, $self->{lang};
475     $country //= $region;
476     my $locale = $language;
477     if ($country && length($country) == 2) {
478         $locale .= '_' . $country;
479     }
480
481     return $locale;
482 }
483
484 sub create_messages {
485     my $self = shift;
486
487     my $pot = "$Bin/$self->{domain}.pot";
488     my $po = "$self->{path_po}/$self->{lang}-messages.po";
489
490     unless ( -f $pot ) {
491         $self->extract_messages();
492     }
493
494     say "Create messages ($self->{lang})" if $self->{verbose};
495     my $locale = $self->locale_name();
496     system "$self->{msginit} -i $pot -o $po -l $locale --no-translator";
497
498     # If msginit failed to correctly set Plural-Forms, set a default one
499     system "$self->{sed} --in-place $po "
500         . "--expression='s/Plural-Forms: nplurals=INTEGER; plural=EXPRESSION/Plural-Forms: nplurals=2; plural=(n != 1)/'";
501 }
502
503 sub update_messages {
504     my $self = shift;
505
506     my $pot = "$Bin/$self->{domain}.pot";
507     my $po = "$self->{path_po}/$self->{lang}-messages.po";
508
509     unless ( -f $pot ) {
510         $self->extract_messages();
511     }
512
513     if ( -f $po ) {
514         say "Update messages ($self->{lang})" if $self->{verbose};
515         system "$self->{msgmerge} --quiet -U $po $pot";
516     } else {
517         $self->create_messages();
518     }
519 }
520
521 sub extract_messages_from_templates {
522     my ($self, $tempdir, $type, @files) = @_;
523
524     my $htdocs = $type eq 'intranet' ? 'intrahtdocs' : 'opachtdocs';
525     my $dir = $self->{context}->config($htdocs);
526     my @keywords = qw(t tx tn txn tnx tp tpx tnp tnpx);
527     my $parser = Template::Parser->new();
528
529     foreach my $file (@files) {
530         say "Extract messages from $file" if $self->{verbose};
531         my $template = read_file(File::Spec->catfile($dir, $file));
532
533         # No need to process a file that doesn't use the i18n.inc file.
534         next unless $template =~ /i18n\.inc/;
535
536         my $data = $parser->parse($template);
537         unless ($data) {
538             warn "Error at $file : " . $parser->error();
539             next;
540         }
541
542         my $destfile = $type eq 'intranet' ?
543             File::Spec->catfile($tempdir, 'koha-tmpl', 'intranet-tmpl', $file) :
544             File::Spec->catfile($tempdir, 'koha-tmpl', 'opac-tmpl', $file);
545
546         make_path(dirname($destfile));
547         open my $fh, '>', $destfile;
548
549         my @blocks = ($data->{BLOCK}, values %{ $data->{DEFBLOCKS} });
550         foreach my $block (@blocks) {
551             my $document = PPI::Document->new(\$block);
552
553             # [% t('foo') %] is compiled to
554             # $output .= $stash->get(['t', ['foo']]);
555             # We try to find all nodes corresponding to keyword (here 't')
556             my $nodes = $document->find(sub {
557                 my ($topnode, $element) = @_;
558
559                 # Filter out non-valid keywords
560                 return 0 unless ($element->isa('PPI::Token::Quote::Single'));
561                 return 0 unless (grep {$element->content eq qq{'$_'}} @keywords);
562
563                 # keyword (e.g. 't') should be the first element of the arrayref
564                 # passed to $stash->get()
565                 return 0 if $element->sprevious_sibling;
566
567                 return 0 unless $element->snext_sibling
568                     && $element->snext_sibling->snext_sibling
569                     && $element->snext_sibling->snext_sibling->isa('PPI::Structure::Constructor');
570
571                 # Check that it's indeed a call to $stash->get()
572                 my $statement = $element->statement->parent->statement->parent->statement;
573                 return 0 unless grep { $_->isa('PPI::Token::Symbol') && $_->content eq '$stash' } $statement->children;
574                 return 0 unless grep { $_->isa('PPI::Token::Operator') && $_->content eq '->' } $statement->children;
575                 return 0 unless grep { $_->isa('PPI::Token::Word') && $_->content eq 'get' } $statement->children;
576
577                 return 1;
578             });
579
580             next unless $nodes;
581
582             # Write the Perl equivalent of calls to t* functions family, so
583             # xgettext can extract the strings correctly
584             foreach my $node (@$nodes) {
585                 my @args = map {
586                     $_->significant && !$_->isa('PPI::Token::Operator') ? $_->content : ()
587                 } $node->snext_sibling->snext_sibling->find_first('PPI::Statement')->children;
588
589                 my $keyword = $node->content;
590                 $keyword =~ s/^'t(.*)'$/__$1/;
591
592                 # Only keep required args to have a clean output
593                 my @required_args = shift @args;
594                 push @required_args, shift @args if $keyword =~ /n/;
595                 push @required_args, shift @args if $keyword =~ /p/;
596
597                 say $fh "$keyword(" . join(', ', @required_args) . ");";
598             }
599
600         }
601
602         close $fh;
603     }
604
605     return $tempdir;
606 }
607
608 sub extract_messages {
609     my $self = shift;
610
611     say "Extract messages into POT file" if $self->{verbose};
612
613     my $intranetdir = $self->{context}->config('intranetdir');
614     my $opacdir = $self->{context}->config('opacdir');
615
616     # Find common ancestor directory
617     my @intranetdirs = File::Spec->splitdir($intranetdir);
618     my @opacdirs = File::Spec->splitdir($opacdir);
619     my @basedirs;
620     while (@intranetdirs and @opacdirs) {
621         my ($dir1, $dir2) = (shift @intranetdirs, shift @opacdirs);
622         last if $dir1 ne $dir2;
623         push @basedirs, $dir1;
624     }
625     my $basedir = File::Spec->catdir(@basedirs);
626
627     my @files_to_scan;
628     my @directories_to_scan = ('.');
629     my @blacklist = map { File::Spec->catdir(@intranetdirs, $_) } qw(blib koha-tmpl skel tmp t);
630     while (@directories_to_scan) {
631         my $dir = shift @directories_to_scan;
632         opendir DIR, File::Spec->catdir($basedir, $dir) or die "Unable to open $dir: $!";
633         foreach my $entry (readdir DIR) {
634             next if $entry =~ /^\./;
635             my $relentry = File::Spec->catfile($dir, $entry);
636             my $abspath = File::Spec->catfile($basedir, $relentry);
637             if (-d $abspath and not grep /^$relentry$/, @blacklist) {
638                 push @directories_to_scan, $relentry;
639             } elsif (-f $abspath and $relentry =~ /\.(pl|pm)$/) {
640                 push @files_to_scan, $relentry;
641             }
642         }
643     }
644
645     my $intrahtdocs = $self->{context}->config('intrahtdocs');
646     my $opachtdocs = $self->{context}->config('opachtdocs');
647
648     my @intranet_tt_files;
649     find(sub {
650         if ($File::Find::dir =~ m|/en/| && $_ =~ m/\.(tt|inc)$/) {
651             my $filename = $File::Find::name;
652             $filename =~ s|^$intrahtdocs/||;
653             push @intranet_tt_files, $filename;
654         }
655     }, $intrahtdocs);
656
657     my @opac_tt_files;
658     find(sub {
659         if ($File::Find::dir =~ m|/en/| && $_ =~ m/\.(tt|inc)$/) {
660             my $filename = $File::Find::name;
661             $filename =~ s|^$opachtdocs/||;
662             push @opac_tt_files, $filename;
663         }
664     }, $opachtdocs);
665
666     my $tempdir = tempdir('Koha-translate-XXXX', TMPDIR => 1, CLEANUP => 1);
667     $self->extract_messages_from_templates($tempdir, 'intranet', @intranet_tt_files);
668     $self->extract_messages_from_templates($tempdir, 'opac', @opac_tt_files);
669
670     @intranet_tt_files = map { File::Spec->catfile('koha-tmpl', 'intranet-tmpl', $_) } @intranet_tt_files;
671     @opac_tt_files = map { File::Spec->catfile('koha-tmpl', 'opac-tmpl', $_) } @opac_tt_files;
672     my @tt_files = grep { -e File::Spec->catfile($tempdir, $_) } @intranet_tt_files, @opac_tt_files;
673
674     push @files_to_scan, @tt_files;
675
676     my $xgettext_cmd = "$self->{xgettext} --force-po -L Perl --from-code=UTF-8 "
677         . "--package-name=Koha --package-version='' "
678         . "-k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 -k__p:1c,2 "
679         . "-k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ -kN__n:1,2 "
680         . "-kN__p:1c,2 -kN__np:1c,2,3 "
681         . "-o $Bin/$self->{domain}.pot -D $tempdir -D $basedir";
682     $xgettext_cmd .= " $_" foreach (@files_to_scan);
683
684     if (system($xgettext_cmd) != 0) {
685         die "system call failed: $xgettext_cmd";
686     }
687
688     my $replace_charset_cmd = "$self->{sed} --in-place " .
689         "$Bin/$self->{domain}.pot " .
690         "--expression='s/charset=CHARSET/charset=UTF-8/'";
691     if (system($replace_charset_cmd) != 0) {
692         die "system call failed: $replace_charset_cmd";
693     }
694 }
695
696 sub install_messages {
697     my ($self) = @_;
698
699     my $locale = $self->locale_name();
700     my $modir = "$self->{path_po}/$locale/LC_MESSAGES";
701     my $pofile = "$self->{path_po}/$self->{lang}-messages.po";
702     my $mofile = "$modir/$self->{domain}.mo";
703
704     if ( not -f $pofile ) {
705         $self->create_messages();
706     }
707     say "Install messages ($locale)" if $self->{verbose};
708     make_path($modir);
709     system "$self->{msgfmt} -o $mofile $pofile";
710 }
711
712 sub remove_pot {
713     my $self = shift;
714
715     unlink "$Bin/$self->{domain}.pot";
716 }
717
718 sub install {
719     my ($self, $files) = @_;
720     return unless $self->{lang};
721     $self->install_tmpl($files) unless $self->{pref_only};
722     $self->install_prefs();
723     $self->install_messages();
724     $self->remove_pot();
725 }
726
727
728 sub get_all_langs {
729     my $self = shift;
730     opendir( my $dh, $self->{path_po} );
731     my @files = grep { $_ =~ /-pref.po$/ }
732         readdir $dh;
733     @files = map { $_ =~ s/-pref.po$//; $_ } @files;
734 }
735
736
737 sub update {
738     my ($self, $files) = @_;
739     my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
740     for my $lang ( @langs ) {
741         $self->set_lang( $lang );
742         $self->update_tmpl($files) unless $self->{pref_only};
743         $self->update_prefs();
744         $self->update_messages();
745     }
746     $self->remove_pot();
747 }
748
749
750 sub create {
751     my ($self, $files) = @_;
752     return unless $self->{lang};
753     $self->create_tmpl($files) unless $self->{pref_only};
754     $self->create_prefs();
755     $self->create_messages();
756     $self->remove_pot();
757 }
758
759
760
761 1;
762
763
764 =head1 NAME
765
766 LangInstaller.pm - Handle templates and preferences translation
767
768 =head1 SYNOPSYS
769
770   my $installer = LangInstaller->new( 'fr-FR' );
771   $installer->create();
772   $installer->update();
773   $installer->install();
774   for my $lang ( @{$installer->{langs} ) {
775     $installer->set_lang( $lan );
776     $installer->install();
777   }
778
779 =head1 METHODS
780
781 =head2 new
782
783 Create a new instance of the installer object. 
784
785 =head2 create
786
787 For the current language, create .po files for templates and preferences based
788 of the english ('en') version.
789
790 =head2 update
791
792 For the current language, update .po files.
793
794 =head2 install
795
796 For the current langage C<$self->{lang}, use .po files to translate the english
797 version of templates and preferences files and copy those files in the
798 appropriate directory.
799
800 =over
801
802 =item translate create F<lang>
803
804 Create 4 kinds of .po files in F<po> subdirectory:
805 (1) one from each theme on opac pages templates,
806 (2) intranet templates,
807 (3) preferences, and
808 (4) one for each MARC dialect.
809
810
811 =over
812
813 =item F<lang>-opac-{theme}.po
814
815 Contains extracted text from english (en) OPAC templates found in
816 <KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
817
818 =item F<lang>-staff-prog.po
819
820 Contains extracted text from english (en) intranet templates found in
821 <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
822
823 =item F<lang>-pref.po
824
825 Contains extracted text from english (en) preferences. They are found in files
826 located in <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/admin/preferences
827 directory.
828
829 =item F<lang>-marc-{MARC}.po
830
831 Contains extracted text from english (en) files from opac and intranet,
832 related with MARC dialects.
833
834 =back
835
836 =item pref-trans update F<lang>
837
838 Update .po files in F<po> directory, named F<lang>-*.po.
839
840 =item pref-trans install F<lang>
841
842 =back
843
844 =cut
845