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
28 use FindBin qw( $Bin );
30 use File::Path qw( make_path );
34 my ($self, $lang) = @_;
36 $self->{lang} = $lang;
37 $self->{po_path_lang} = $self->{context}->config('intrahtdocs') .
38 "/prog/$lang/modules/admin/preferences";
42 my ($class, $lang, $pref_only, $verbose) = @_;
46 my $context = C4::Context->new();
47 $self->{context} = $context;
48 $self->{path_pref_en} = $context->config('intrahtdocs') .
49 '/prog/en/modules/admin/preferences';
50 set_lang( $self, $lang ) if $lang;
51 $self->{pref_only} = $pref_only;
52 $self->{verbose} = $verbose;
53 $self->{process} = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
54 $self->{path_po} = "$Bin/po";
56 $self->{domain} = 'Koha';
57 $self->{msgfmt} = `which msgfmt`;
58 $self->{po2json} = "$Bin/po2json";
59 $self->{gzip} = `which gzip`;
60 $self->{gunzip} = `which gunzip`;
61 chomp $self->{msgfmt};
63 chomp $self->{gunzip};
65 # Get all .pref file names
66 opendir my $fh, $self->{path_pref_en};
67 my @pref_files = grep { /\.pref$/ } readdir($fh);
69 $self->{pref_files} = \@pref_files;
71 # Get all available language codes
72 opendir $fh, $self->{path_po};
73 my @langs = map { ($_) =~ /(.*)-pref/ }
74 grep { $_ =~ /.*-pref/ } readdir($fh);
76 $self->{langs} = \@langs;
78 # Map for both interfaces opac/intranet
79 my $opachtdocs = $context->config('opachtdocs');
80 $self->{interface} = [
82 name => 'Intranet prog UI',
83 dir => $context->config('intrahtdocs') . '/prog',
84 suffix => '-staff-prog.po',
89 opendir my $dh, $context->config('opachtdocs');
90 for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
91 push @{$self->{interface}}, {
92 name => "OPAC $theme",
93 dir => "$opachtdocs/$theme",
94 suffix => "-opac-$theme.po",
98 # MARC flavours (hardcoded list)
99 for ( "MARC21", "UNIMARC", "NORMARC" ) {
100 # search for strings on staff & opac marc files
101 my $dirs = $context->config('intrahtdocs') . '/prog';
102 opendir $fh, $context->config('opachtdocs');
103 for ( grep { not /^\.|\.\.|lib$|xslt/ } readdir($fh) ) {
104 $dirs .= ' ' . "$opachtdocs/$_";
106 push @{$self->{interface}}, {
109 suffix => "-marc-$_.po",
113 # EN YAML installer files
114 push @{$self->{installer}}, {
115 name => "YAML installer files",
116 dirs => [ 'installer/data/mysql/en/mandatory',
117 'installer/data/mysql/en/optional'],
118 suffix => "-installer.po",
121 # EN MARC21 YAML installer files
122 push @{$self->{installer}}, {
123 name => "MARC21 YAML installer files",
124 dirs => [ 'installer/data/mysql/en/marcflavour/marc21/mandatory',
125 'installer/data/mysql/en/marcflavour/marc21/optional'],
126 suffix => "-installer-MARC21.po",
129 # EN UNIMARC YAML installer files
130 push @{$self->{installer}}, {
131 name => "UNIMARC YAML installer files",
132 dirs => [ 'installer/data/mysql/en/marcflavour/unimarc/mandatory', ],
133 suffix => "-installer-UNIMARC.po",
143 my $context = C4::Context->new;
144 my $trans_path = $Bin . '/po';
145 my $trans_file = "$trans_path/" . $self->{lang} . $suffix;
150 my ($self, $msgid, $default) = @_;
152 my $po = $self->{po}->{Locale::PO->quote($msgid)};
154 my $msgstr = Locale::PO->dequote($po->msgstr);
155 if ($msgstr and length($msgstr) > 0) {
163 sub get_translated_tab_content {
164 my ($self, $file, $tab_content) = @_;
166 if ( ref($tab_content) eq 'ARRAY' ) {
167 return $self->get_translated_prefs($file, $tab_content);
170 my $translated_tab_content = {
173 my $sysprefs = $tab_content->{$section};
174 my $msgid = sprintf('%s %s', $file, $section);
176 $self->get_trans_text($msgid, $section) => $self->get_translated_prefs($file, $sysprefs);
180 return $translated_tab_content;
183 sub get_translated_prefs {
184 my ($self, $file, $sysprefs) = @_;
186 my $translated_prefs = [
188 my ($pref_elt) = grep { ref($_) eq 'HASH' && exists $_->{pref} } @$_;
189 my $pref_name = $pref_elt ? $pref_elt->{pref} : '';
191 my $translated_syspref = [
193 $self->get_translated_pref($file, $pref_name, $_);
201 return $translated_prefs;
204 sub get_translated_pref {
205 my ($self, $file, $pref_name, $syspref) = @_;
207 unless (ref($syspref)) {
209 my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $syspref);
210 return $self->get_trans_text($msgid, $syspref);
213 my $translated_pref = {
216 my $value = $syspref->{$key};
218 my $translated_value = $value;
219 if (($key eq 'choices' || $key eq 'multiple') && ref($value) eq 'HASH') {
220 $translated_value = {
222 my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $value->{$_});
223 $_ => $self->get_trans_text($msgid, $value->{$_})
228 $key => $translated_value
232 return $translated_pref;
238 unless ( -r $self->{po_path_lang} ) {
239 print "Koha directories hierarchy for ", $self->{lang}, " must be created first\n";
243 $self->{po} = Locale::PO->load_file_ashash($self->po_filename("-pref.po"), 'utf8');
245 for my $file ( @{$self->{pref_files}} ) {
246 my $pref = YAML::XS::LoadFile( $self->{path_pref_en} . "/$file" );
248 my $translated_pref = {
251 my $tab_content = $pref->{$tab};
253 $self->get_trans_text($file, $tab) => $self->get_translated_tab_content($file, $tab_content);
258 my $file_trans = $self->{po_path_lang} . "/$file";
259 print "Write $file\n" if $self->{verbose};
260 YAML::XS::DumpFile($file_trans, $translated_pref);
266 my ($self, $files) = @_;
267 say "Install templates" if $self->{verbose};
268 for my $trans ( @{$self->{interface}} ) {
269 my @t_dirs = split(" ", $trans->{dir});
270 for my $t_dir ( @t_dirs ) {
274 " Install templates '$trans->{name}'\n",
275 " From: $t_dir/en/\n",
276 " To : $t_dir/$self->{lang}\n",
277 " With: $self->{path_po}/$self->{lang}$trans->{suffix}\n"
280 my $trans_dir = "$t_dir/en/";
281 my $lang_dir = "$t_dir/$self->{lang}";
282 $lang_dir =~ s|/en/|/$self->{lang}/|;
283 mkdir $lang_dir unless -d $lang_dir;
284 # if installing MARC po file, only touch corresponding files
285 my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations
286 # if not installing MARC po file, ignore all MARC files
287 @nomarc = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
290 "$self->{process} install " .
293 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
295 ( @files ? ' -f ' . join ' -f ', @files : '') .
296 ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
306 my $po_file = $self->po_filename( $target->{suffix} );
307 return $srcyml unless ( -e $po_file );
309 my $po_ref = Locale::PO->load_file_ashash( $po_file, 'utf8' );
311 my $dstyml = YAML::XS::LoadFile( $srcyml );
313 # translate fields in table rows
314 my @tables = @{ $dstyml->{'tables'} };
315 for my $table ( @tables ) { # each table
316 my $table_name = ( keys %$table )[0];
317 my @translatable = @{ $table->{$table_name}->{translatable} };
318 my @rows = @{ $table->{$table_name}->{rows} };
319 my @multiline = @{ $table->{$table_name}->{'multiline'} }; # to check multiline values
320 for my $row ( @rows ) { # each row
321 for my $field ( @translatable ) { # each translatable field
322 if ( @multiline and grep { $_ eq $field } @multiline ) { # multiline fields, only notices ATM
323 foreach my $line ( @{$row->{$field}} ) {
324 next if ( $line =~ /^(\s*<.*?>\s*$|^\s*\[.*?\]\s*|\s*)$/ ); # discard pure html, TT, empty
326 while ( $line =~ s/(<<.*?>>|\[\%.*?\%\]|<.*?>)/\%s/ ) { # put placeholders, save matches
331 if ( $line =~ /^(\s|%s|-|[[:punct:]]|\(|\))*$/ ) { # ignore non strings
332 while ( @ttvar ) { # restore placeholders
333 my $var = shift @ttvar;
334 $line =~ s/\%s/$var/;
338 my $po = $po_ref->{"\"$line\""}; # quoted key
339 if ( $po and not defined( $po->fuzzy() ) # not fuzzy
340 and length( $po->msgid() ) > 2 # not empty msgid
341 and length( $po->msgstr() ) > 2 ) { # not empty msgstr
342 $line = $po->dequote( $po->msgstr() );
344 while ( @ttvar ) { # restore placeholders
345 my $var = shift @ttvar;
346 $line =~ s/\%s/$var/;
351 next unless defined $row->{$field}; # next if null value
352 my $po = $po_ref->{"\"$row->{$field}\""}; # quoted key
353 if ( $po and not defined( $po->fuzzy() ) # not fuzzy
354 and length( $po->msgid() ) > 2 # not empty msgid
355 and length( $po->msgstr() ) > 2 ) { # not empty msgstr
356 $row->{$field} = $po->dequote( $po->msgstr() );
363 # translate descriptions
364 for my $description ( @{ $dstyml->{'description'} } ) {
365 my $po = $po_ref->{"\"$description\""};
366 if ( $po and not defined( $po->fuzzy() )
367 and length( $po->msgid() ) > 2
368 and length( $po->msgstr() ) > 2 ) {
369 $description = $po->dequote( $po->msgstr() );
376 sub install_installer {
378 return unless ( $self->{installer} );
380 my $intradir = $self->{context}->config('intranetdir');
381 my $db_scheme = $self->{context}->config('db_scheme');
382 my $langdir = "$intradir/installer/data/$db_scheme/$self->{lang}";
384 say "$self->{lang} installer dir $langdir already exists.\nDelete it if you want to recreate it." if $self->{verbose};
388 say "Install installer files\n" if $self->{verbose};
390 for my $target ( @{ $self->{installer} } ) {
391 return unless ( -e $self->po_filename( $target->{suffix} ) );
392 for my $dir ( @{ $target->{dirs} } ) {
393 ( my $tdir = "$dir" ) =~ s|/en/|/$self->{lang}/|;
394 make_path("$intradir/$tdir");
396 opendir( my $dh, "$intradir/$dir" ) or die ("Can't open $intradir/$dir");
397 my @files = grep { ! /^\.+$/ } readdir($dh);
400 for my $file ( @files ) {
401 if ( $file =~ /yml$/ ) {
402 my $translated_yaml = translate_yaml( $self, $target, "$intradir/$dir/$file" );
403 YAML::XS::DumpFile( "$intradir/$tdir/$file", $translated_yaml );
405 File::Copy::copy( "$intradir/$dir/$file", "$intradir/$tdir/$file" );
415 my ($language, $region, $country) = split /-/, $self->{lang};
416 $country //= $region;
417 my $locale = $language;
418 if ($country && length($country) == 2) {
419 $locale .= '_' . $country;
425 sub install_messages {
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";
434 unless ( -f $pofile && -f $js_pofile ) {
435 die "PO files for language '$self->{lang}' do not exist";
438 say "Install messages ($locale)" if $self->{verbose};
440 system "$self->{msgfmt} -o $mofile $pofile";
442 my $js_locale_data = 'var json_locale_data = {"Koha":' . `$self->{po2json} $js_pofile` . '};';
443 my $progdir = $self->{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;
449 my $opachtdocs = $self->{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;
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";
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";
488 my ($self, $files) = @_;
489 return unless $self->{lang};
492 if ($self->{pref_only}) {
493 $self->install_prefs();
495 $self->install_tmpl($files);
496 $self->install_prefs();
497 $self->install_messages();
498 $self->install_installer();
505 opendir( my $dh, $self->{path_po} );
506 my @files = grep { $_ =~ /-pref.(po|po.gz)$/ }
508 @files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files;
516 LangInstaller.pm - Handle templates and preferences translation
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();
533 Create a new instance of the installer object.
537 For the current language, create .po files for templates and preferences based
538 of the english ('en') version.
542 For the current language, update .po files.
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.
552 =item translate create F<lang>
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,
558 (4) one for each MARC dialect.
563 =item F<lang>-opac-{theme}.po
565 Contains extracted text from english (en) OPAC templates found in
566 <KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
568 =item F<lang>-staff-prog.po
570 Contains extracted text from english (en) intranet templates found in
571 <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
573 =item F<lang>-pref.po
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
579 =item F<lang>-marc-{MARC}.po
581 Contains extracted text from english (en) files from opac and intranet,
582 related with MARC dialects.
586 =item pref-trans update F<lang>
588 Update .po files in F<po> directory, named F<lang>-*.po.
590 =item pref-trans install F<lang>