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 );
29 use File::Path qw( make_path );
33 my ($self, $lang) = @_;
35 $self->{lang} = $lang;
36 $self->{po_path_lang} = C4::Context->config('intrahtdocs') .
37 "/prog/$lang/modules/admin/preferences";
41 my ($class, $lang, $pref_only, $verbose) = @_;
45 $self->{path_pref_en} = C4::Context->config('intrahtdocs') .
46 '/prog/en/modules/admin/preferences';
47 set_lang( $self, $lang ) if $lang;
48 $self->{pref_only} = $pref_only;
49 $self->{verbose} = $verbose;
50 $self->{process} = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
51 $self->{path_po} = "$Bin/po";
53 $self->{domain} = 'Koha';
54 $self->{msgfmt} = `which msgfmt`;
55 $self->{po2json} = "$Bin/po2json";
56 $self->{gzip} = `which gzip`;
57 $self->{gunzip} = `which gunzip`;
58 chomp $self->{msgfmt};
60 chomp $self->{gunzip};
62 # Get all .pref file names
63 opendir my $fh, $self->{path_pref_en};
64 my @pref_files = grep { /\.pref$/ } readdir($fh);
66 $self->{pref_files} = \@pref_files;
68 # Get all available language codes
69 opendir $fh, $self->{path_po};
70 my @langs = map { ($_) =~ /(.*)-pref/ }
71 grep { $_ =~ /.*-pref/ } readdir($fh);
73 $self->{langs} = \@langs;
75 # Map for both interfaces opac/intranet
76 my $opachtdocs = C4::Context->config('opachtdocs');
77 $self->{interface} = [
79 name => 'Intranet prog UI',
80 dir => C4::Context->config('intrahtdocs') . '/prog',
81 suffix => '-staff-prog.po',
86 opendir my $dh, C4::Context->config('opachtdocs');
87 for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
88 push @{$self->{interface}}, {
89 name => "OPAC $theme",
90 dir => "$opachtdocs/$theme",
91 suffix => "-opac-$theme.po",
95 # MARC flavours (hardcoded list)
96 for ( "MARC21", "UNIMARC" ) {
97 # search for strings on staff & opac marc files
98 my $dirs = C4::Context->config('intrahtdocs') . '/prog';
99 opendir $fh, C4::Context->config('opachtdocs');
100 for ( grep { not /^\.|\.\.|lib$|xslt/ } readdir($fh) ) {
101 $dirs .= ' ' . "$opachtdocs/$_";
103 push @{$self->{interface}}, {
106 suffix => "-marc-$_.po",
110 # EN YAML installer files
111 push @{$self->{installer}}, {
112 name => "YAML installer files",
113 dirs => [ 'installer/data/mysql/en/mandatory',
114 'installer/data/mysql/en/optional'],
115 suffix => "-installer.po",
118 # EN MARC21 YAML installer files
119 push @{$self->{installer}}, {
120 name => "MARC21 YAML installer files",
121 dirs => [ 'installer/data/mysql/en/marcflavour/marc21/mandatory',
122 'installer/data/mysql/en/marcflavour/marc21/optional'],
123 suffix => "-installer-MARC21.po",
126 # EN UNIMARC YAML installer files
127 push @{$self->{installer}}, {
128 name => "UNIMARC YAML installer files",
129 dirs => [ 'installer/data/mysql/en/marcflavour/unimarc/mandatory',
130 'installer/data/mysql/en/marcflavour/unimarc/optional'],
131 suffix => "-installer-UNIMARC.po",
141 my $trans_path = $Bin . '/po';
142 my $trans_file = "$trans_path/" . $self->{lang} . $suffix;
147 my ($self, $msgid, $default) = @_;
149 my $po = $self->{po}->{Locale::PO->quote($msgid)};
150 if ( $po and not defined( $po->fuzzy() ) ) {
151 my $msgstr = Locale::PO->dequote($po->msgstr);
152 if ($msgstr and length($msgstr) > 0) {
160 sub get_translated_tab_content {
161 my ($self, $file, $tab_content) = @_;
163 if ( ref($tab_content) eq 'ARRAY' ) {
164 return $self->get_translated_prefs($file, $tab_content);
167 my $translated_tab_content = {
170 my $sysprefs = $tab_content->{$section};
171 my $msgid = sprintf('%s %s', $file, $section);
173 $self->get_trans_text($msgid, $section) => $self->get_translated_prefs($file, $sysprefs);
177 if ( keys %$translated_tab_content != keys %$tab_content ) {
179 for my $section (keys %$tab_content) {
180 push @{$duplicates{$self->get_trans_text("$file $section", $section)}}, $section;
182 for my $translation (keys %duplicates) {
183 if (@{$duplicates{$translation}} > 1) {
184 warn qq(In file "$file", "$translation" is a translation for sections ") . join('", "', @{$duplicates{$translation}}) . '"';
189 return $translated_tab_content;
192 sub get_translated_prefs {
193 my ($self, $file, $sysprefs) = @_;
195 my $translated_prefs = [
197 my ($pref_elt) = grep { ref($_) eq 'HASH' && exists $_->{pref} } @$_;
198 my $pref_name = $pref_elt ? $pref_elt->{pref} : '';
200 my $translated_syspref = [
202 $self->get_translated_pref($file, $pref_name, $_);
210 return $translated_prefs;
213 sub get_translated_pref {
214 my ($self, $file, $pref_name, $syspref) = @_;
216 unless (ref($syspref)) {
218 my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $syspref);
219 return $self->get_trans_text($msgid, $syspref);
222 my $translated_pref = {
225 my $value = $syspref->{$key};
227 my $translated_value = $value;
228 if (($key eq 'choices' || $key eq 'multiple') && ref($value) eq 'HASH') {
229 $translated_value = {
231 my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $value->{$_});
232 $_ => $self->get_trans_text($msgid, $value->{$_})
237 $key => $translated_value
241 return $translated_pref;
247 unless ( -r $self->{po_path_lang} ) {
248 print "Koha directories hierarchy for ", $self->{lang}, " must be created first\n";
252 $self->{po} = Locale::PO->load_file_ashash($self->po_filename("-pref.po"), 'utf8');
254 for my $file ( @{$self->{pref_files}} ) {
255 my $pref = YAML::XS::LoadFile( $self->{path_pref_en} . "/$file" );
257 my $translated_pref = {
260 my $tab_content = $pref->{$tab};
262 $self->get_trans_text($file, $tab) => $self->get_translated_tab_content($file, $tab_content);
266 if ( keys %$translated_pref != keys %$pref ) {
268 for my $tab (keys %$pref) {
269 push @{$duplicates{$self->get_trans_text($file, $tab)}}, $tab;
271 for my $translation (keys %duplicates) {
272 if (@{$duplicates{$translation}} > 1) {
273 warn qq(In file "$file", "$translation" is a translation for tabs ") . join('", "', @{$duplicates{$translation}}) . '"';
278 my $file_trans = $self->{po_path_lang} . "/$file";
279 print "Write $file\n" if $self->{verbose};
280 YAML::XS::DumpFile($file_trans, $translated_pref);
286 my ($self, $files) = @_;
287 say "Install templates" if $self->{verbose};
288 for my $trans ( @{$self->{interface}} ) {
289 my @t_dirs = split(" ", $trans->{dir});
290 for my $t_dir ( @t_dirs ) {
294 " Install templates '$trans->{name}'\n",
295 " From: $t_dir/en/\n",
296 " To : $t_dir/$self->{lang}\n",
297 " With: $self->{path_po}/$self->{lang}$trans->{suffix}\n"
300 my $trans_dir = "$t_dir/en/";
301 my $lang_dir = "$t_dir/$self->{lang}";
302 $lang_dir =~ s|/en/|/$self->{lang}/|;
303 mkdir $lang_dir unless -d $lang_dir;
304 # if installing MARC po file, only touch corresponding files
305 my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations
306 # if not installing MARC po file, ignore all MARC files
307 @nomarc = ( 'marc21', 'unimarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
310 "$self->{process} install " .
313 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
315 ( @files ? ' -f ' . join ' -f ', @files : '') .
316 ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
326 my $po_file = $self->po_filename( $target->{suffix} );
327 return $srcyml unless ( -e $po_file );
329 my $po_ref = Locale::PO->load_file_ashash( $po_file, 'utf8' );
331 my $dstyml = YAML::XS::LoadFile( $srcyml );
333 # translate fields in table rows
334 my @tables = @{ $dstyml->{'tables'} };
335 for my $table ( @tables ) { # each table
336 my $table_name = ( keys %$table )[0];
337 my @translatable = @{ $table->{$table_name}->{translatable} };
338 my @rows = @{ $table->{$table_name}->{rows} };
339 my @multiline = @{ $table->{$table_name}->{'multiline'} }; # to check multiline values
340 for my $row ( @rows ) { # each row
341 for my $field ( @translatable ) { # each translatable field
342 if ( @multiline and grep { $_ eq $field } @multiline ) { # multiline fields, only notices ATM
343 foreach my $line ( @{$row->{$field}} ) {
345 while ( $line =~ s/(<<.*?>>|\[\%.*?\%\]|<.*?>)/\%s/ ) { # put placeholders, save matches
350 if ( $line =~ /^(\s|%s|-|[[:punct:]]|\(|\))*$/ ) { # ignore non strings
351 while ( @ttvar ) { # restore placeholders
352 my $var = shift @ttvar;
353 $line =~ s/\%s/$var/;
357 my $po = $po_ref->{"\"$line\""}; # quoted key
358 if ( $po and not defined( $po->fuzzy() ) # not fuzzy
359 and length( $po->msgid() ) > 2 # not empty msgid
360 and length( $po->msgstr() ) > 2 ) { # not empty msgstr
361 $line = $po->dequote( $po->msgstr() );
363 while ( @ttvar ) { # restore placeholders
364 my $var = shift @ttvar;
365 $line =~ s/\%s/$var/;
370 next unless defined $row->{$field}; # next if null value
371 my $po = $po_ref->{"\"$row->{$field}\""}; # quoted key
372 if ( $po and not defined( $po->fuzzy() ) # not fuzzy
373 and length( $po->msgid() ) > 2 # not empty msgid
374 and length( $po->msgstr() ) > 2 ) { # not empty msgstr
375 $row->{$field} = $po->dequote( $po->msgstr() );
382 # translate descriptions
383 for my $description ( @{ $dstyml->{'description'} } ) {
384 my $po = $po_ref->{"\"$description\""};
385 if ( $po and not defined( $po->fuzzy() )
386 and length( $po->msgid() ) > 2
387 and length( $po->msgstr() ) > 2 ) {
388 $description = $po->dequote( $po->msgstr() );
395 sub install_installer {
397 return unless ( $self->{installer} );
399 my $intradir = C4::Context->config('intranetdir');
400 my $db_scheme = C4::Context->config('db_scheme');
401 my $langdir = "$intradir/installer/data/$db_scheme/$self->{lang}";
403 say "Install installer files\n" if $self->{verbose};
405 for my $target ( @{ $self->{installer} } ) {
406 return unless ( -e $self->po_filename( $target->{suffix} ) );
407 for my $dir ( @{ $target->{dirs} } ) {
408 ( my $tdir = "$dir" ) =~ s|/en/|/$self->{lang}/|;
409 make_path("$intradir/$tdir");
411 opendir( my $dh, "$intradir/$dir" ) or die ("Can't open $intradir/$dir");
412 my @files = grep { ! /^\.+$/ } readdir($dh);
415 for my $file ( @files ) {
416 if ( $file =~ /yml$/ ) {
417 my $translated_yaml = translate_yaml( $self, $target, "$intradir/$dir/$file" );
418 YAML::XS::DumpFile( "$intradir/$tdir/$file", $translated_yaml );
420 File::Copy::copy( "$intradir/$dir/$file", "$intradir/$tdir/$file" );
430 my ($language, $region, $country) = split /-/, $self->{lang};
431 $country //= $region;
432 my $locale = $language;
433 if ($country && length($country) == 2) {
434 $locale .= '_' . $country;
440 sub install_messages {
443 my $locale = $self->locale_name();
444 my $modir = "$self->{path_po}/$locale/LC_MESSAGES";
445 my $pofile = "$self->{path_po}/$self->{lang}-messages.po";
446 my $mofile = "$modir/$self->{domain}.mo";
447 my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po";
449 unless ( -f $pofile && -f $js_pofile ) {
450 die "PO files for language '$self->{lang}' do not exist";
453 say "Install messages ($locale)" if $self->{verbose};
455 system "$self->{msgfmt} -o $mofile $pofile";
457 my $js_locale_data = 'var json_locale_data = {"Koha":' . `$self->{po2json} $js_pofile` . '};';
458 my $progdir = C4::Context->config('intrahtdocs') . '/prog';
459 mkdir "$progdir/$self->{lang}/js";
460 open my $fh, '>', "$progdir/$self->{lang}/js/locale_data.js";
461 print $fh $js_locale_data;
464 my $opachtdocs = C4::Context->config('opachtdocs');
465 opendir(my $dh, $opachtdocs);
466 for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
467 mkdir "$opachtdocs/$theme/$self->{lang}/js";
468 open my $fh, '>', "$opachtdocs/$theme/$self->{lang}/js/locale_data.js";
469 print $fh $js_locale_data;
475 my ($self, $files) = @_;
476 my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
477 for my $lang ( @langs ) {
478 $self->set_lang( $lang );
479 opendir( my $dh, $self->{path_po} );
480 my @files = grep { $_ =~ /^$self->{lang}.*po$/ } readdir $dh;
481 foreach my $file ( @files ) {
482 say "Compress file $file" if $self->{verbose};
483 system "$self->{gzip} -9 $self->{path_po}/$file";
489 my ($self, $files) = @_;
490 my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
491 for my $lang ( @langs ) {
492 opendir( my $dh, $self->{path_po} );
493 $self->set_lang( $lang );
494 my @files = grep { $_ =~ /^$self->{lang}.*po.gz$/ } readdir $dh;
495 foreach my $file ( @files ) {
496 say "Uncompress file $file" if $self->{verbose};
497 system "$self->{gunzip} $self->{path_po}/$file";
503 my ($self, $files) = @_;
504 return unless $self->{lang};
507 if ($self->{pref_only}) {
508 $self->install_prefs();
510 $self->install_tmpl($files);
511 $self->install_prefs();
512 $self->install_messages();
513 $self->install_installer();
520 opendir( my $dh, $self->{path_po} );
521 my @files = grep { $_ =~ /-pref.(po|po.gz)$/ }
523 @files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files;
531 LangInstaller.pm - Handle templates and preferences translation
535 my $installer = LangInstaller->new( 'fr-FR' );
536 $installer->create();
537 $installer->update();
538 $installer->install();
539 for my $lang ( @{$installer->{langs} ) {
540 $installer->set_lang( $lan );
541 $installer->install();
548 Create a new instance of the installer object.
552 For the current language, create .po files for templates and preferences based
553 of the english ('en') version.
557 For the current language, update .po files.
561 For the current langage C<$self->{lang}, use .po files to translate the english
562 version of templates and preferences files and copy those files in the
563 appropriate directory.
567 =item translate create F<lang>
569 Create 4 kinds of .po files in F<po> subdirectory:
570 (1) one from each theme on opac pages templates,
571 (2) intranet templates,
573 (4) one for each MARC dialect.
578 =item F<lang>-opac-{theme}.po
580 Contains extracted text from english (en) OPAC templates found in
581 <KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
583 =item F<lang>-staff-prog.po
585 Contains extracted text from english (en) intranet templates found in
586 <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
588 =item F<lang>-pref.po
590 Contains extracted text from english (en) preferences. They are found in files
591 located in <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/admin/preferences
594 =item F<lang>-marc-{MARC}.po
596 Contains extracted text from english (en) files from opac and intranet,
597 related with MARC dialects.
601 =item pref-trans update F<lang>
603 Update .po files in F<po> directory, named F<lang>-*.po.
605 =item pref-trans install F<lang>