3 # Copyright (C) 2010 Tamil s.a.r.l.
5 # This file is part of Koha.
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.
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.
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>.
23 # WARNING: Any other tested YAML library fails to work properly in this
25 use YAML
::Syck
qw( Dump LoadFile );
27 use FindBin
qw( $Bin );
29 $YAML::Syck::ImplicitTyping = 1;
32 # Default file header for .po syspref files
33 my $default_pref_po_header = Locale::PO->new(-msgid => '', -msgstr =>
34 "Project-Id-Version: PACKAGE VERSION\\n" .
35 "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\n" .
36 "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n" .
37 "Language-Team: Koha Translate List <koha-translate\@lists.koha-community.org>\\n" .
38 "MIME-Version: 1.0\\n" .
39 "Content-Type: text/plain; charset=UTF-8\\n" .
40 "Content-Transfer-Encoding: 8bit\\n" .
41 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
46 my ($self, $lang) = @_;
48 $self->{lang} = $lang;
49 $self->{po_path_lang} = $self->{context}->config('intrahtdocs') .
50 "/prog/$lang/modules/admin/preferences";
55 my ($class, $lang, $pref_only, $verbose) = @_;
59 my $context = C4::Context->new();
60 $self->{context} = $context;
61 $self->{path_pref_en} = $context->config('intrahtdocs') .
62 '/prog/en/modules/admin/preferences';
63 set_lang( $self, $lang ) if $lang;
64 $self->{pref_only} = $pref_only;
65 $self->{verbose} = $verbose;
66 $self->{process} = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
67 $self->{path_po} = "$Bin/po";
68 $self->{po} = { '' => $default_pref_po_header };
69 $self->{domain} = 'messages';
70 $self->{cp} = `which cp`;
71 $self->{msgmerge} = `which msgmerge`;
72 $self->{xgettext} = `which xgettext`;
73 $self->{sed} = `which sed`;
75 chomp $self->{msgmerge};
76 chomp $self->{xgettext};
79 # Get all .pref file names
80 opendir my $fh, $self->{path_pref_en};
81 my @pref_files = grep { /.pref/ } readdir($fh);
83 $self->{pref_files} = \@pref_files;
85 # Get all available language codes
86 opendir $fh, $self->{path_po};
87 my @langs = map { ($_) =~ /(.*)-pref/ }
88 grep { $_ =~ /.*-pref/ } readdir($fh);
90 $self->{langs} = \@langs;
92 # Map for both interfaces opac/intranet
93 my $opachtdocs = $context->config('opachtdocs');
94 $self->{interface} = [
96 name => 'Intranet prog UI',
97 dir => $context->config('intrahtdocs') . '/prog',
98 suffix => '-staff-prog.po',
101 name => 'Intranet prog help',
102 dir => $context->config('intrahtdocs') . '/prog/en/modules/help',
103 suffix => '-staff-help.po',
108 opendir my $dh, $context->config('opachtdocs');
109 for my $theme ( grep { not /^\.|lib/ } readdir($dh) ) {
110 push @{$self->{interface}}, {
111 name => "OPAC $theme",
112 dir => "$opachtdocs/$theme",
113 suffix => "-opac-$theme.po",
117 # MARC flavours (hardcoded list)
118 for ( "MARC21", "UNIMARC", "NORMARC" ) {
119 # search for strings on staff & opac marc files
120 my $dirs = $context->config('intrahtdocs') . '/prog';
121 opendir $fh, $context->config('opachtdocs');
122 for ( grep { not /^\.|\.\.|lib$/ } readdir($fh) ) {
123 $dirs .= ' ' . "$opachtdocs/$_";
125 push @{$self->{interface}}, {
128 suffix => "-marc-$_.po",
139 my $context = C4::Context->new;
140 my $trans_path = $Bin . '/po';
141 my $trans_file = "$trans_path/" . $self->{lang} . "-pref.po";
147 my ($self, $id, $comment) = @_;
148 my $po = $self->{po};
151 $p->comment( $p->comment . "\n" . $comment );
154 $po->{$id} = Locale::PO->new(
155 -comment => $comment,
164 my ($self, $comment, $prefs) = @_;
166 for my $pref ( @$prefs ) {
168 for my $element ( @$pref ) {
169 if ( ref( $element) eq 'HASH' ) {
170 $pref_name = $element->{pref};
174 for my $element ( @$pref ) {
175 if ( ref( $element) eq 'HASH' ) {
176 while ( my ($key, $value) = each(%$element) ) {
177 next unless $key eq 'choices';
178 next unless ref($value) eq 'HASH';
179 for my $ckey ( keys %$value ) {
180 my $id = $self->{file} . "#$pref_name# " . $value->{$ckey};
181 $self->po_append( $id, $comment );
186 $self->po_append( $self->{file} . "#$pref_name# $element", $comment );
194 my ($self, $id) = @_;
196 my $po = $self->{po}->{$id};
198 return Locale::PO->dequote($po->msgstr);
202 sub update_tab_prefs {
203 my ($self, $pref, $prefs) = @_;
205 for my $p ( @$prefs ) {
208 for my $element ( @$p ) {
209 if ( ref( $element) eq 'HASH' ) {
210 $pref_name = $element->{pref};
214 for my $i ( 0..@$p-1 ) {
215 my $element = $p->[$i];
216 if ( ref( $element) eq 'HASH' ) {
217 while ( my ($key, $value) = each(%$element) ) {
218 next unless $key eq 'choices';
219 next unless ref($value) eq 'HASH';
220 for my $ckey ( keys %$value ) {
221 my $id = $self->{file} . "#$pref_name# " . $value->{$ckey};
222 my $text = $self->get_trans_text( $id );
223 $value->{$ckey} = $text if $text;
228 my $id = $self->{file} . "#$pref_name# $element";
229 my $text = $self->get_trans_text( $id );
230 $p->[$i] = $text if $text;
237 sub get_po_from_prefs {
240 for my $file ( @{$self->{pref_files}} ) {
241 my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
242 $self->{file} = $file;
243 # Entries for tab titles
244 $self->po_append( $self->{file}, $_ ) for keys %$pref;
245 while ( my ($tab, $tab_content) = each %$pref ) {
246 if ( ref($tab_content) eq 'ARRAY' ) {
247 $self->add_prefs( $tab, $tab_content );
250 while ( my ($section, $sysprefs) = each %$tab_content ) {
251 my $comment = "$tab > $section";
252 $self->po_append( $self->{file} . " " . $section, $comment );
253 $self->add_prefs( $comment, $sysprefs );
263 # Create file header if it doesn't already exist
264 my $po = $self->{po};
265 $po->{''} ||= $default_pref_po_header;
267 # Write .po entries into a file put in Koha standard po directory
268 Locale::PO->save_file_fromhash( $self->po_filename, $po );
269 say "Saved in file: ", $self->po_filename if $self->{verbose};
273 sub get_po_merged_with_en {
276 # Get po from current 'en' .pref files
277 $self->get_po_from_prefs();
278 my $po_current = $self->{po};
280 # Get po from previous generation
281 my $po_previous = Locale::PO->load_file_ashash( $self->po_filename );
283 for my $id ( keys %$po_current ) {
284 my $po = $po_previous->{Locale::PO->quote($id)};
286 my $text = Locale::PO->dequote( $po->msgstr );
287 $po_current->{$id}->msgstr( $text );
294 print "Update '", $self->{lang},
295 "' preferences .po file from 'en' .pref files\n" if $self->{verbose};
296 $self->get_po_merged_with_en();
304 unless ( -r $self->{po_path_lang} ) {
305 print "Koha directories hierarchy for ", $self->{lang}, " must be created first\n";
309 # Get the language .po file merged with last modified 'en' preferences
310 $self->get_po_merged_with_en();
312 for my $file ( @{$self->{pref_files}} ) {
313 my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
314 $self->{file} = $file;
315 # First, keys are replaced (tab titles)
318 $self->get_trans_text( $self->{file} ) || $_ => $pref->{$_}
322 while ( my ($tab, $tab_content) = each %$pref ) {
323 if ( ref($tab_content) eq 'ARRAY' ) {
324 $self->update_tab_prefs( $pref, $tab_content );
327 while ( my ($section, $sysprefs) = each %$tab_content ) {
328 $self->update_tab_prefs( $pref, $sysprefs );
331 for my $section ( keys %$tab_content ) {
332 my $id = $self->{file} . " $section";
333 my $text = $self->get_trans_text($id);
334 my $nsection = $text ? $text : $section;
335 $ntab->{$nsection} = $tab_content->{$section};
337 $pref->{$tab} = $ntab;
339 my $file_trans = $self->{po_path_lang} . "/$file";
340 print "Write $file\n" if $self->{verbose};
341 open my $fh, ">", $file_trans;
342 print $fh Dump($pref);
348 my ($self, $files) = @_;
349 say "Install templates" if $self->{verbose};
350 for my $trans ( @{$self->{interface}} ) {
351 my @t_dirs = split(" ", $trans->{dir});
352 for my $t_dir ( @t_dirs ) {
356 " Install templates '$trans->{name}'\n",
357 " From: $t_dir/en/\n",
358 " To : $t_dir/$self->{lang}\n",
359 " With: $self->{path_po}/$self->{lang}$trans->{suffix}\n"
362 my $trans_dir = ( $trans->{name} =~ /help/ )?"$t_dir":"$t_dir/en/";
363 my $lang_dir = ( $trans->{name} =~ /help/ )?"$t_dir":"$t_dir/$self->{lang}";
364 $lang_dir =~ s|/en/|/$self->{lang}/|;
365 mkdir $lang_dir unless -d $lang_dir;
366 my $excludes = ( $trans->{name} !~ /help/ )?"":"-x 'help'";
367 # if installing MARC po file, only touch corresponding files
368 my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations
369 # if not installing MARC po file, ignore all MARC files
370 @nomarc = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
373 "$self->{process} install " .
376 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
379 ( @files ? ' -f ' . join ' -f ', @files : '') .
380 ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
387 my ($self, $files) = @_;
389 say "Update templates" if $self->{verbose};
390 for my $trans ( @{$self->{interface}} ) {
394 " Update templates '$trans->{name}'\n",
395 " From: $trans->{dir}/en/\n",
396 " To : $self->{path_po}/$self->{lang}$trans->{suffix}\n"
399 my $trans_dir = ( $trans->{name} =~ /help/ )?"$trans->{dir}":join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs
400 # do no process 'help' dirs unless needed
401 my $excludes = ( $trans->{name} !~ /help/ )?"-x help":"";
402 # if processing MARC po file, only use corresponding files
403 my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations
404 # if not processing MARC po file, ignore all MARC files
405 @nomarc = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
408 "$self->{process} update " .
410 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
413 ( @files ? ' -f ' . join ' -f ', @files : '') .
414 ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
422 if ( -e $self->po_filename ) {
423 say "Preferences .po file already exists. Delete it if you want to recreate it.";
426 $self->get_po_from_prefs();
432 my ($self, $files) = @_;
434 say "Create templates\n" if $self->{verbose};
435 for my $trans ( @{$self->{interface}} ) {
439 " Create templates .po files for '$trans->{name}'\n",
440 " From: $trans->{dir}/en/\n",
441 " To : $self->{path_po}/$self->{lang}$trans->{suffix}\n"
444 my $trans_dir = ( $trans->{name} =~ /help/ )?"$trans->{dir}":join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs
445 my $excludes = ( $trans->{name} !~ /help/ )?"-x help":"";
446 # if processing MARC po file, only use corresponding files
447 my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations
448 # if not processing MARC po file, ignore all MARC files
449 @nomarc = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
452 "$self->{process} create " .
454 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
457 ( @files ? ' -f ' . join ' -f ', @files : '') .
458 ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
462 sub create_messages {
465 print "Create messages ($self->{lang})\n" if $self->{verbose};
467 "$self->{cp} $self->{domain}.pot " .
468 "$self->{path_po}/$self->{lang}-$self->{domain}.po";
471 sub update_messages {
474 my $pofile = "$self->{path_po}/$self->{lang}-$self->{domain}.po";
475 print "Update messages ($self->{lang})\n" if $self->{verbose};
476 if ( not -f $pofile ) {
477 print "File $pofile does not exist\n" if $self->{verbose};
478 $self->create_messages();
480 system "$self->{msgmerge} -U $pofile $self->{domain}.pot";
483 sub extract_messages {
486 my $intranetdir = $self->{context}->config('intranetdir');
488 my @directories_to_scan = ('.');
489 my @blacklist = qw(blib koha-tmpl skel tmp t);
490 while (@directories_to_scan) {
491 my $dir = shift @directories_to_scan;
492 opendir DIR
, "$intranetdir/$dir" or die "Unable to open $dir: $!";
493 foreach my $entry (readdir DIR
) {
494 next if $entry =~ /^\./;
495 my $relentry = "$dir/$entry";
496 $relentry =~ s
|^\
./||;
497 if (-d
"$intranetdir/$relentry" and not grep /^$relentry$/, @blacklist) {
498 push @directories_to_scan, "$relentry";
499 } elsif (-f
"$intranetdir/$relentry" and $relentry =~ /(pl
|pm
)$/) {
500 push @files_to_scan, "$relentry";
505 my $xgettext_cmd = "$self->{xgettext} -L Perl --from-code=UTF-8 " .
506 "-o $Bin/$self->{domain}.pot -D $intranetdir";
507 $xgettext_cmd .= " $_" foreach (@files_to_scan);
509 if (system($xgettext_cmd) != 0) {
510 die "system call failed: $xgettext_cmd";
513 if ( -f
"$Bin/$self->{domain}.pot" ) {
514 my $replace_charset_cmd = "$self->{sed} --in-place " .
515 "$Bin/$self->{domain}.pot " .
516 "--expression='s/charset=CHARSET/charset=UTF-8/'";
517 if (system($replace_charset_cmd) != 0) {
518 die "system call failed: $replace_charset_cmd";
521 print "No messages found\n" if $self->{verbose
};
530 unlink "$Bin/$self->{domain}.pot";
534 my ($self, $files) = @_;
535 return unless $self->{lang
};
536 $self->install_tmpl($files) unless $self->{pref_only
};
537 $self->install_prefs();
543 opendir( my $dh, $self->{path_po
} );
544 my @files = grep { $_ =~ /-pref.po$/ }
546 @files = map { $_ =~ s/-pref.po$//; $_ } @files;
551 my ($self, $files) = @_;
552 my @langs = $self->{lang
} ?
($self->{lang
}) : $self->get_all_langs();
553 my $extract_ok = $self->extract_messages();
554 for my $lang ( @langs ) {
555 $self->set_lang( $lang );
556 $self->update_tmpl($files) unless $self->{pref_only
};
557 $self->update_prefs();
558 $self->update_messages() if $extract_ok;
560 $self->remove_pot() if $extract_ok;
565 my ($self, $files) = @_;
566 return unless $self->{lang
};
567 $self->create_tmpl($files) unless $self->{pref_only
};
568 $self->create_prefs();
569 if ($self->extract_messages()) {
570 $self->create_messages();
582 LangInstaller.pm - Handle templates and preferences translation
586 my $installer = LangInstaller->new( 'fr-FR' );
587 $installer->create();
588 $installer->update();
589 $installer->install();
590 for my $lang ( @{$installer->{langs} ) {
591 $installer->set_lang( $lan );
592 $installer->install();
599 Create a new instance of the installer object.
603 For the current language, create .po files for templates and preferences based
604 of the english ('en') version.
608 For the current language, update .po files.
612 For the current langage C<$self->{lang}, use .po files to translate the english
613 version of templates and preferences files and copy those files in the
614 appropriate directory.
618 =item translate create F<lang>
620 Create 4 kinds of .po files in F<po> subdirectory:
621 (1) one from each theme on opac pages templates,
622 (2) intranet templates and help,
624 (4) one for each MARC dialect.
629 =item F<lang>-opac-{theme}.po
631 Contains extracted text from english (en) OPAC templates found in
632 <KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
634 =item F<lang>-staff-prog.po and F<lang>-staff-help.po
636 Contains extracted text from english (en) intranet templates found in
637 <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
639 =item F<lang>-pref.po
641 Contains extracted text from english (en) preferences. They are found in files
642 located in <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/admin/preferences
645 =item F<lang>-marc-{MARC}.po
647 Contains extracted text from english (en) files from opac and intranet,
648 related with MARC dialects.
652 =item pref-trans update F<lang>
654 Update .po files in F<po> directory, named F<lang>-*.po.
656 =item pref-trans install F<lang>