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 );
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 open(my $fh, ">:encoding(UTF-8)", "$intradir/$tdir/$file");
404 YAML::XS::DumpFile( $fh, $translated_yaml );
407 File::Copy::copy( "$intradir/$dir/$file", "$intradir/$tdir/$file" );
417 my ($language, $region, $country) = split /-/, $self->{lang};
418 $country //= $region;
419 my $locale = $language;
420 if ($country && length($country) == 2) {
421 $locale .= '_' . $country;
427 sub install_messages {
430 my $locale = $self->locale_name();
431 my $modir = "$self->{path_po}/$locale/LC_MESSAGES";
432 my $pofile = "$self->{path_po}/$self->{lang}-messages.po";
433 my $mofile = "$modir/$self->{domain}.mo";
434 my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po";
436 unless ( -f $pofile && -f $js_pofile ) {
437 die "PO files for language '$self->{lang}' do not exist";
440 say "Install messages ($locale)" if $self->{verbose};
442 system "$self->{msgfmt} -o $mofile $pofile";
444 my $js_locale_data = 'var json_locale_data = {"Koha":' . `$self->{po2json} $js_pofile` . '};';
445 my $progdir = $self->{context}->config('intrahtdocs') . '/prog';
446 mkdir "$progdir/$self->{lang}/js";
447 open my $fh, '>', "$progdir/$self->{lang}/js/locale_data.js";
448 print $fh $js_locale_data;
451 my $opachtdocs = $self->{context}->config('opachtdocs');
452 opendir(my $dh, $opachtdocs);
453 for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
454 mkdir "$opachtdocs/$theme/$self->{lang}/js";
455 open my $fh, '>', "$opachtdocs/$theme/$self->{lang}/js/locale_data.js";
456 print $fh $js_locale_data;
462 my ($self, $files) = @_;
463 my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
464 for my $lang ( @langs ) {
465 $self->set_lang( $lang );
466 opendir( my $dh, $self->{path_po} );
467 my @files = grep { $_ =~ /^$self->{lang}.*po$/ } readdir $dh;
468 foreach my $file ( @files ) {
469 say "Compress file $file" if $self->{verbose};
470 system "$self->{gzip} -9 $self->{path_po}/$file";
476 my ($self, $files) = @_;
477 my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
478 for my $lang ( @langs ) {
479 opendir( my $dh, $self->{path_po} );
480 $self->set_lang( $lang );
481 my @files = grep { $_ =~ /^$self->{lang}.*po.gz$/ } readdir $dh;
482 foreach my $file ( @files ) {
483 say "Uncompress file $file" if $self->{verbose};
484 system "$self->{gunzip} $self->{path_po}/$file";
490 my ($self, $files) = @_;
491 return unless $self->{lang};
494 if ($self->{pref_only}) {
495 $self->install_prefs();
497 $self->install_tmpl($files);
498 $self->install_prefs();
499 $self->install_messages();
500 $self->install_installer();
507 opendir( my $dh, $self->{path_po} );
508 my @files = grep { $_ =~ /-pref.(po|po.gz)$/ }
510 @files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files;
518 LangInstaller.pm - Handle templates and preferences translation
522 my $installer = LangInstaller->new( 'fr-FR' );
523 $installer->create();
524 $installer->update();
525 $installer->install();
526 for my $lang ( @{$installer->{langs} ) {
527 $installer->set_lang( $lan );
528 $installer->install();
535 Create a new instance of the installer object.
539 For the current language, create .po files for templates and preferences based
540 of the english ('en') version.
544 For the current language, update .po files.
548 For the current langage C<$self->{lang}, use .po files to translate the english
549 version of templates and preferences files and copy those files in the
550 appropriate directory.
554 =item translate create F<lang>
556 Create 4 kinds of .po files in F<po> subdirectory:
557 (1) one from each theme on opac pages templates,
558 (2) intranet templates,
560 (4) one for each MARC dialect.
565 =item F<lang>-opac-{theme}.po
567 Contains extracted text from english (en) OPAC templates found in
568 <KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
570 =item F<lang>-staff-prog.po
572 Contains extracted text from english (en) intranet templates found in
573 <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
575 =item F<lang>-pref.po
577 Contains extracted text from english (en) preferences. They are found in files
578 located in <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/admin/preferences
581 =item F<lang>-marc-{MARC}.po
583 Contains extracted text from english (en) files from opac and intranet,
584 related with MARC dialects.
588 =item pref-trans update F<lang>
590 Update .po files in F<po> directory, named F<lang>-*.po.
592 =item pref-trans install F<lang>