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} = C4::Context->config('intrahtdocs') .
38 "/prog/$lang/modules/admin/preferences";
42 my ($class, $lang, $pref_only, $verbose) = @_;
46 $self->{path_pref_en} = C4::Context->config('intrahtdocs') .
47 '/prog/en/modules/admin/preferences';
48 set_lang( $self, $lang ) if $lang;
49 $self->{pref_only} = $pref_only;
50 $self->{verbose} = $verbose;
51 $self->{process} = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
52 $self->{path_po} = "$Bin/po";
54 $self->{domain} = 'Koha';
55 $self->{msgfmt} = `which msgfmt`;
56 $self->{po2json} = "$Bin/po2json";
57 $self->{gzip} = `which gzip`;
58 $self->{gunzip} = `which gunzip`;
59 chomp $self->{msgfmt};
61 chomp $self->{gunzip};
63 # Get all .pref file names
64 opendir my $fh, $self->{path_pref_en};
65 my @pref_files = grep { /\.pref$/ } readdir($fh);
67 $self->{pref_files} = \@pref_files;
69 # Get all available language codes
70 opendir $fh, $self->{path_po};
71 my @langs = map { ($_) =~ /(.*)-pref/ }
72 grep { $_ =~ /.*-pref/ } readdir($fh);
74 $self->{langs} = \@langs;
76 # Map for both interfaces opac/intranet
77 my $opachtdocs = C4::Context->config('opachtdocs');
78 $self->{interface} = [
80 name => 'Intranet prog UI',
81 dir => C4::Context->config('intrahtdocs') . '/prog',
82 suffix => '-staff-prog.po',
87 opendir my $dh, C4::Context->config('opachtdocs');
88 for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
89 push @{$self->{interface}}, {
90 name => "OPAC $theme",
91 dir => "$opachtdocs/$theme",
92 suffix => "-opac-$theme.po",
96 # MARC flavours (hardcoded list)
97 for ( "MARC21", "UNIMARC", "NORMARC" ) {
98 # search for strings on staff & opac marc files
99 my $dirs = C4::Context->config('intrahtdocs') . '/prog';
100 opendir $fh, C4::Context->config('opachtdocs');
101 for ( grep { not /^\.|\.\.|lib$|xslt/ } readdir($fh) ) {
102 $dirs .= ' ' . "$opachtdocs/$_";
104 push @{$self->{interface}}, {
107 suffix => "-marc-$_.po",
111 # EN YAML installer files
112 push @{$self->{installer}}, {
113 name => "YAML installer files",
114 dirs => [ 'installer/data/mysql/en/mandatory',
115 'installer/data/mysql/en/optional'],
116 suffix => "-installer.po",
119 # EN MARC21 YAML installer files
120 push @{$self->{installer}}, {
121 name => "MARC21 YAML installer files",
122 dirs => [ 'installer/data/mysql/en/marcflavour/marc21/mandatory',
123 'installer/data/mysql/en/marcflavour/marc21/optional'],
124 suffix => "-installer-MARC21.po",
127 # EN UNIMARC YAML installer files
128 push @{$self->{installer}}, {
129 name => "UNIMARC YAML installer files",
130 dirs => [ 'installer/data/mysql/en/marcflavour/unimarc/mandatory', ],
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)};
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 return $translated_tab_content;
180 sub get_translated_prefs {
181 my ($self, $file, $sysprefs) = @_;
183 my $translated_prefs = [
185 my ($pref_elt) = grep { ref($_) eq 'HASH' && exists $_->{pref} } @$_;
186 my $pref_name = $pref_elt ? $pref_elt->{pref} : '';
188 my $translated_syspref = [
190 $self->get_translated_pref($file, $pref_name, $_);
198 return $translated_prefs;
201 sub get_translated_pref {
202 my ($self, $file, $pref_name, $syspref) = @_;
204 unless (ref($syspref)) {
206 my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $syspref);
207 return $self->get_trans_text($msgid, $syspref);
210 my $translated_pref = {
213 my $value = $syspref->{$key};
215 my $translated_value = $value;
216 if (($key eq 'choices' || $key eq 'multiple') && ref($value) eq 'HASH') {
217 $translated_value = {
219 my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $value->{$_});
220 $_ => $self->get_trans_text($msgid, $value->{$_})
225 $key => $translated_value
229 return $translated_pref;
235 unless ( -r $self->{po_path_lang} ) {
236 print "Koha directories hierarchy for ", $self->{lang}, " must be created first\n";
240 $self->{po} = Locale::PO->load_file_ashash($self->po_filename("-pref.po"), 'utf8');
242 for my $file ( @{$self->{pref_files}} ) {
243 my $pref = YAML::XS::LoadFile( $self->{path_pref_en} . "/$file" );
245 my $translated_pref = {
248 my $tab_content = $pref->{$tab};
250 $self->get_trans_text($file, $tab) => $self->get_translated_tab_content($file, $tab_content);
255 my $file_trans = $self->{po_path_lang} . "/$file";
256 print "Write $file\n" if $self->{verbose};
257 YAML::XS::DumpFile($file_trans, $translated_pref);
263 my ($self, $files) = @_;
264 say "Install templates" if $self->{verbose};
265 for my $trans ( @{$self->{interface}} ) {
266 my @t_dirs = split(" ", $trans->{dir});
267 for my $t_dir ( @t_dirs ) {
271 " Install templates '$trans->{name}'\n",
272 " From: $t_dir/en/\n",
273 " To : $t_dir/$self->{lang}\n",
274 " With: $self->{path_po}/$self->{lang}$trans->{suffix}\n"
277 my $trans_dir = "$t_dir/en/";
278 my $lang_dir = "$t_dir/$self->{lang}";
279 $lang_dir =~ s|/en/|/$self->{lang}/|;
280 mkdir $lang_dir unless -d $lang_dir;
281 # if installing MARC po file, only touch corresponding files
282 my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations
283 # if not installing MARC po file, ignore all MARC files
284 @nomarc = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
287 "$self->{process} install " .
290 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
292 ( @files ? ' -f ' . join ' -f ', @files : '') .
293 ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
303 my $po_file = $self->po_filename( $target->{suffix} );
304 return $srcyml unless ( -e $po_file );
306 my $po_ref = Locale::PO->load_file_ashash( $po_file, 'utf8' );
308 my $dstyml = YAML::XS::LoadFile( $srcyml );
310 # translate fields in table rows
311 my @tables = @{ $dstyml->{'tables'} };
312 for my $table ( @tables ) { # each table
313 my $table_name = ( keys %$table )[0];
314 my @translatable = @{ $table->{$table_name}->{translatable} };
315 my @rows = @{ $table->{$table_name}->{rows} };
316 my @multiline = @{ $table->{$table_name}->{'multiline'} }; # to check multiline values
317 for my $row ( @rows ) { # each row
318 for my $field ( @translatable ) { # each translatable field
319 if ( @multiline and grep { $_ eq $field } @multiline ) { # multiline fields, only notices ATM
320 foreach my $line ( @{$row->{$field}} ) {
321 next if ( $line =~ /^(\s*<.*?>\s*$|^\s*\[.*?\]\s*|\s*)$/ ); # discard pure html, TT, empty
323 while ( $line =~ s/(<<.*?>>|\[\%.*?\%\]|<.*?>)/\%s/ ) { # put placeholders, save matches
328 if ( $line =~ /^(\s|%s|-|[[:punct:]]|\(|\))*$/ ) { # ignore non strings
329 while ( @ttvar ) { # restore placeholders
330 my $var = shift @ttvar;
331 $line =~ s/\%s/$var/;
335 my $po = $po_ref->{"\"$line\""}; # quoted key
336 if ( $po and not defined( $po->fuzzy() ) # not fuzzy
337 and length( $po->msgid() ) > 2 # not empty msgid
338 and length( $po->msgstr() ) > 2 ) { # not empty msgstr
339 $line = $po->dequote( $po->msgstr() );
341 while ( @ttvar ) { # restore placeholders
342 my $var = shift @ttvar;
343 $line =~ s/\%s/$var/;
348 next unless defined $row->{$field}; # next if null value
349 my $po = $po_ref->{"\"$row->{$field}\""}; # quoted key
350 if ( $po and not defined( $po->fuzzy() ) # not fuzzy
351 and length( $po->msgid() ) > 2 # not empty msgid
352 and length( $po->msgstr() ) > 2 ) { # not empty msgstr
353 $row->{$field} = $po->dequote( $po->msgstr() );
360 # translate descriptions
361 for my $description ( @{ $dstyml->{'description'} } ) {
362 my $po = $po_ref->{"\"$description\""};
363 if ( $po and not defined( $po->fuzzy() )
364 and length( $po->msgid() ) > 2
365 and length( $po->msgstr() ) > 2 ) {
366 $description = $po->dequote( $po->msgstr() );
373 sub install_installer {
375 return unless ( $self->{installer} );
377 my $intradir = C4::Context->config('intranetdir');
378 my $db_scheme = C4::Context->config('db_scheme');
379 my $langdir = "$intradir/installer/data/$db_scheme/$self->{lang}";
381 say "$self->{lang} installer dir $langdir already exists.\nDelete it if you want to recreate it." if $self->{verbose};
385 say "Install installer files\n" if $self->{verbose};
387 for my $target ( @{ $self->{installer} } ) {
388 return unless ( -e $self->po_filename( $target->{suffix} ) );
389 for my $dir ( @{ $target->{dirs} } ) {
390 ( my $tdir = "$dir" ) =~ s|/en/|/$self->{lang}/|;
391 make_path("$intradir/$tdir");
393 opendir( my $dh, "$intradir/$dir" ) or die ("Can't open $intradir/$dir");
394 my @files = grep { ! /^\.+$/ } readdir($dh);
397 for my $file ( @files ) {
398 if ( $file =~ /yml$/ ) {
399 my $translated_yaml = translate_yaml( $self, $target, "$intradir/$dir/$file" );
400 YAML::XS::DumpFile( "$intradir/$tdir/$file", $translated_yaml );
402 File::Copy::copy( "$intradir/$dir/$file", "$intradir/$tdir/$file" );
412 my ($language, $region, $country) = split /-/, $self->{lang};
413 $country //= $region;
414 my $locale = $language;
415 if ($country && length($country) == 2) {
416 $locale .= '_' . $country;
422 sub install_messages {
425 my $locale = $self->locale_name();
426 my $modir = "$self->{path_po}/$locale/LC_MESSAGES";
427 my $pofile = "$self->{path_po}/$self->{lang}-messages.po";
428 my $mofile = "$modir/$self->{domain}.mo";
429 my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po";
431 unless ( -f $pofile && -f $js_pofile ) {
432 die "PO files for language '$self->{lang}' do not exist";
435 say "Install messages ($locale)" if $self->{verbose};
437 system "$self->{msgfmt} -o $mofile $pofile";
439 my $js_locale_data = 'var json_locale_data = {"Koha":' . `$self->{po2json} $js_pofile` . '};';
440 my $progdir = C4::Context->config('intrahtdocs') . '/prog';
441 mkdir "$progdir/$self->{lang}/js";
442 open my $fh, '>', "$progdir/$self->{lang}/js/locale_data.js";
443 print $fh $js_locale_data;
446 my $opachtdocs = C4::Context->config('opachtdocs');
447 opendir(my $dh, $opachtdocs);
448 for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
449 mkdir "$opachtdocs/$theme/$self->{lang}/js";
450 open my $fh, '>', "$opachtdocs/$theme/$self->{lang}/js/locale_data.js";
451 print $fh $js_locale_data;
457 my ($self, $files) = @_;
458 my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
459 for my $lang ( @langs ) {
460 $self->set_lang( $lang );
461 opendir( my $dh, $self->{path_po} );
462 my @files = grep { $_ =~ /^$self->{lang}.*po$/ } readdir $dh;
463 foreach my $file ( @files ) {
464 say "Compress file $file" if $self->{verbose};
465 system "$self->{gzip} -9 $self->{path_po}/$file";
471 my ($self, $files) = @_;
472 my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
473 for my $lang ( @langs ) {
474 opendir( my $dh, $self->{path_po} );
475 $self->set_lang( $lang );
476 my @files = grep { $_ =~ /^$self->{lang}.*po.gz$/ } readdir $dh;
477 foreach my $file ( @files ) {
478 say "Uncompress file $file" if $self->{verbose};
479 system "$self->{gunzip} $self->{path_po}/$file";
485 my ($self, $files) = @_;
486 return unless $self->{lang};
489 if ($self->{pref_only}) {
490 $self->install_prefs();
492 $self->install_tmpl($files);
493 $self->install_prefs();
494 $self->install_messages();
495 $self->install_installer();
502 opendir( my $dh, $self->{path_po} );
503 my @files = grep { $_ =~ /-pref.(po|po.gz)$/ }
505 @files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files;
513 LangInstaller.pm - Handle templates and preferences translation
517 my $installer = LangInstaller->new( 'fr-FR' );
518 $installer->create();
519 $installer->update();
520 $installer->install();
521 for my $lang ( @{$installer->{langs} ) {
522 $installer->set_lang( $lan );
523 $installer->install();
530 Create a new instance of the installer object.
534 For the current language, create .po files for templates and preferences based
535 of the english ('en') version.
539 For the current language, update .po files.
543 For the current langage C<$self->{lang}, use .po files to translate the english
544 version of templates and preferences files and copy those files in the
545 appropriate directory.
549 =item translate create F<lang>
551 Create 4 kinds of .po files in F<po> subdirectory:
552 (1) one from each theme on opac pages templates,
553 (2) intranet templates,
555 (4) one for each MARC dialect.
560 =item F<lang>-opac-{theme}.po
562 Contains extracted text from english (en) OPAC templates found in
563 <KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
565 =item F<lang>-staff-prog.po
567 Contains extracted text from english (en) intranet templates found in
568 <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
570 =item F<lang>-pref.po
572 Contains extracted text from english (en) preferences. They are found in files
573 located in <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/admin/preferences
576 =item F<lang>-marc-{MARC}.po
578 Contains extracted text from english (en) files from opac and intranet,
579 related with MARC dialects.
583 =item pref-trans update F<lang>
585 Update .po files in F<po> directory, named F<lang>-*.po.
587 =item pref-trans install F<lang>