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( LoadFile DumpFile );
27 use FindBin qw( $Bin );
29 use File::Path qw( make_path );
32 $YAML::Syck::ImplicitTyping = 1;
35 my ($self, $lang) = @_;
37 $self->{lang} = $lang;
38 $self->{po_path_lang} = C4::Context->config('intrahtdocs') .
39 "/prog/$lang/modules/admin/preferences";
43 my ($class, $lang, $pref_only, $verbose) = @_;
47 $self->{path_pref_en} = C4::Context->config('intrahtdocs') .
48 '/prog/en/modules/admin/preferences';
49 set_lang( $self, $lang ) if $lang;
50 $self->{pref_only} = $pref_only;
51 $self->{verbose} = $verbose;
52 $self->{process} = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
53 $self->{path_po} = "$Bin/po";
55 $self->{domain} = 'Koha';
56 $self->{msgfmt} = `which msgfmt`;
57 $self->{po2json} = "$Bin/po2json";
58 $self->{gzip} = `which gzip`;
59 $self->{gunzip} = `which gunzip`;
60 chomp $self->{msgfmt};
62 chomp $self->{gunzip};
64 # Get all .pref file names
65 opendir my $fh, $self->{path_pref_en};
66 my @pref_files = grep { /\.pref$/ } readdir($fh);
68 $self->{pref_files} = \@pref_files;
70 # Get all available language codes
71 opendir $fh, $self->{path_po};
72 my @langs = map { ($_) =~ /(.*)-pref/ }
73 grep { $_ =~ /.*-pref/ } readdir($fh);
75 $self->{langs} = \@langs;
77 # Map for both interfaces opac/intranet
78 my $opachtdocs = C4::Context->config('opachtdocs');
79 $self->{interface} = [
81 name => 'Intranet prog UI',
82 dir => C4::Context->config('intrahtdocs') . '/prog',
83 suffix => '-staff-prog.po',
88 opendir my $dh, C4::Context->config('opachtdocs');
89 for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
90 push @{$self->{interface}}, {
91 name => "OPAC $theme",
92 dir => "$opachtdocs/$theme",
93 suffix => "-opac-$theme.po",
97 # MARC flavours (hardcoded list)
98 for ( "MARC21", "UNIMARC", "NORMARC" ) {
99 # search for strings on staff & opac marc files
100 my $dirs = C4::Context->config('intrahtdocs') . '/prog';
101 opendir $fh, C4::Context->config('opachtdocs');
102 for ( grep { not /^\.|\.\.|lib$|xslt/ } readdir($fh) ) {
103 $dirs .= ' ' . "$opachtdocs/$_";
105 push @{$self->{interface}}, {
108 suffix => "-marc-$_.po",
112 # EN YAML installer files
113 push @{$self->{installer}}, {
114 name => "YAML installer files",
115 dirs => [ 'installer/data/mysql/en/mandatory',
116 'installer/data/mysql/en/optional'],
117 suffix => "-installer.po",
120 # EN MARC21 YAML installer files
121 push @{$self->{installer}}, {
122 name => "MARC21 YAML installer files",
123 dirs => [ 'installer/data/mysql/en/marcflavour/marc21/mandatory',
124 'installer/data/mysql/en/marcflavour/marc21/optional'],
125 suffix => "-installer-MARC21.po",
128 # EN UNIMARC YAML installer files
129 push @{$self->{installer}}, {
130 name => "UNIMARC YAML installer files",
131 dirs => [ 'installer/data/mysql/en/marcflavour/unimarc/mandatory', ],
132 suffix => "-installer-UNIMARC.po",
142 my $trans_path = $Bin . '/po';
143 my $trans_file = "$trans_path/" . $self->{lang} . $suffix;
148 my ($self, $msgid, $default) = @_;
150 my $po = $self->{po}->{Locale::PO->quote($msgid)};
152 my $msgstr = Locale::PO->dequote($po->msgstr);
153 if ($msgstr and length($msgstr) > 0) {
161 sub get_translated_tab_content {
162 my ($self, $file, $tab_content) = @_;
164 if ( ref($tab_content) eq 'ARRAY' ) {
165 return $self->get_translated_prefs($file, $tab_content);
168 my $translated_tab_content = {
171 my $sysprefs = $tab_content->{$section};
172 my $msgid = sprintf('%s %s', $file, $section);
174 $self->get_trans_text($msgid, $section) => $self->get_translated_prefs($file, $sysprefs);
178 return $translated_tab_content;
181 sub get_translated_prefs {
182 my ($self, $file, $sysprefs) = @_;
184 my $translated_prefs = [
186 my ($pref_elt) = grep { ref($_) eq 'HASH' && exists $_->{pref} } @$_;
187 my $pref_name = $pref_elt ? $pref_elt->{pref} : '';
189 my $translated_syspref = [
191 $self->get_translated_pref($file, $pref_name, $_);
199 return $translated_prefs;
202 sub get_translated_pref {
203 my ($self, $file, $pref_name, $syspref) = @_;
205 unless (ref($syspref)) {
207 my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $syspref);
208 return $self->get_trans_text($msgid, $syspref);
211 my $translated_pref = {
214 my $value = $syspref->{$key};
216 my $translated_value = $value;
217 if (($key eq 'choices' || $key eq 'multiple') && ref($value) eq 'HASH') {
218 $translated_value = {
220 my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $value->{$_});
221 $_ => $self->get_trans_text($msgid, $value->{$_})
226 $key => $translated_value
230 return $translated_pref;
236 unless ( -r $self->{po_path_lang} ) {
237 print "Koha directories hierarchy for ", $self->{lang}, " must be created first\n";
241 $self->{po} = Locale::PO->load_file_ashash($self->po_filename("-pref.po"), 'utf8');
243 for my $file ( @{$self->{pref_files}} ) {
244 my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
246 my $translated_pref = {
249 my $tab_content = $pref->{$tab};
251 $self->get_trans_text($file, $tab) => $self->get_translated_tab_content($file, $tab_content);
256 my $file_trans = $self->{po_path_lang} . "/$file";
257 print "Write $file\n" if $self->{verbose};
258 DumpFile($file_trans, $translated_pref);
264 my ($self, $files) = @_;
265 say "Install templates" if $self->{verbose};
266 for my $trans ( @{$self->{interface}} ) {
267 my @t_dirs = split(" ", $trans->{dir});
268 for my $t_dir ( @t_dirs ) {
272 " Install templates '$trans->{name}'\n",
273 " From: $t_dir/en/\n",
274 " To : $t_dir/$self->{lang}\n",
275 " With: $self->{path_po}/$self->{lang}$trans->{suffix}\n"
278 my $trans_dir = "$t_dir/en/";
279 my $lang_dir = "$t_dir/$self->{lang}";
280 $lang_dir =~ s|/en/|/$self->{lang}/|;
281 mkdir $lang_dir unless -d $lang_dir;
282 # if installing MARC po file, only touch corresponding files
283 my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations
284 # if not installing MARC po file, ignore all MARC files
285 @nomarc = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
288 "$self->{process} install " .
291 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
293 ( @files ? ' -f ' . join ' -f ', @files : '') .
294 ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
304 my $po_file = $self->po_filename( $target->{suffix} );
305 return $srcyml unless ( -e $po_file );
307 my $po_ref = Locale::PO->load_file_ashash( $po_file );
309 my $dstyml = LoadFile( $srcyml );
311 # translate fields in table rows
312 my @tables = @{ $dstyml->{'tables'} };
313 for my $table ( @tables ) { # each table
314 my $table_name = ( keys %$table )[0];
315 my @translatable = @{ $table->{$table_name}->{translatable} };
316 my @rows = @{ $table->{$table_name}->{rows} };
317 my @multiline = @{ $table->{$table_name}->{'multiline'} }; # to check multiline values
318 for my $row ( @rows ) { # each row
319 for my $field ( @translatable ) { # each translatable field
320 if ( @multiline and grep { $_ eq $field } @multiline ) { # multiline fields, only notices ATM
321 foreach my $line ( @{$row->{$field}} ) {
322 next if ( $line =~ /^(\s*<.*?>\s*$|^\s*\[.*?\]\s*|\s*)$/ ); # discard pure html, TT, empty
324 while ( $line =~ s/(<<.*?>>|\[\%.*?\%\]|<.*?>)/\%s/ ) { # put placeholders, save matches
329 if ( $line =~ /^(\s|%s|-|[[:punct:]]|\(|\))*$/ ) { # ignore non strings
330 while ( @ttvar ) { # restore placeholders
331 my $var = shift @ttvar;
332 $line =~ s/\%s/$var/;
336 my $po = $po_ref->{"\"$line\""}; # quoted key
337 if ( $po and not defined( $po->fuzzy() ) # not fuzzy
338 and length( $po->msgid() ) > 2 # not empty msgid
339 and length( $po->msgstr() ) > 2 ) { # not empty msgstr
340 $line = $po->dequote( $po->msgstr() );
342 while ( @ttvar ) { # restore placeholders
343 my $var = shift @ttvar;
344 $line =~ s/\%s/$var/;
349 next unless defined $row->{$field}; # next if null value
350 my $po = $po_ref->{"\"$row->{$field}\""}; # quoted key
351 if ( $po and not defined( $po->fuzzy() ) # not fuzzy
352 and length( $po->msgid() ) > 2 # not empty msgid
353 and length( $po->msgstr() ) > 2 ) { # not empty msgstr
354 $row->{$field} = $po->dequote( $po->msgstr() );
361 # translate descriptions
362 for my $description ( @{ $dstyml->{'description'} } ) {
363 my $po = $po_ref->{"\"$description\""};
364 if ( $po and not defined( $po->fuzzy() )
365 and length( $po->msgid() ) > 2
366 and length( $po->msgstr() ) > 2 ) {
367 $description = $po->dequote( $po->msgstr() );
374 sub install_installer {
376 return unless ( $self->{installer} );
378 my $intradir = C4::Context->config('intranetdir');
379 my $db_scheme = C4::Context->config('db_scheme');
380 my $langdir = "$intradir/installer/data/$db_scheme/$self->{lang}";
382 say "$self->{lang} installer dir $langdir already exists.\nDelete it if you want to recreate it." if $self->{verbose};
386 say "Install installer files\n" if $self->{verbose};
388 for my $target ( @{ $self->{installer} } ) {
389 return unless ( -e $self->po_filename( $target->{suffix} ) );
390 for my $dir ( @{ $target->{dirs} } ) {
391 ( my $tdir = "$dir" ) =~ s|/en/|/$self->{lang}/|;
392 make_path("$intradir/$tdir");
394 opendir( my $dh, "$intradir/$dir" ) or die ("Can't open $intradir/$dir");
395 my @files = grep { ! /^\.+$/ } readdir($dh);
398 for my $file ( @files ) {
399 if ( $file =~ /yml$/ ) {
400 my $translated_yaml = translate_yaml( $self, $target, "$intradir/$dir/$file" );
401 open(my $fh, ">:encoding(UTF-8)", "$intradir/$tdir/$file");
402 DumpFile( $fh, $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 = C4::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 = C4::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>