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} = $self->{context}->config('intrahtdocs') .
39 "/prog/$lang/modules/admin/preferences";
43 my ($class, $lang, $pref_only, $verbose) = @_;
47 my $context = C4::Context->new();
48 $self->{context} = $context;
49 $self->{path_pref_en} = $context->config('intrahtdocs') .
50 '/prog/en/modules/admin/preferences';
51 set_lang( $self, $lang ) if $lang;
52 $self->{pref_only} = $pref_only;
53 $self->{verbose} = $verbose;
54 $self->{process} = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
55 $self->{path_po} = "$Bin/po";
57 $self->{domain} = 'Koha';
58 $self->{msgfmt} = `which msgfmt`;
59 $self->{po2json} = "$Bin/po2json";
60 $self->{gzip} = `which gzip`;
61 $self->{gunzip} = `which gunzip`;
62 chomp $self->{msgfmt};
64 chomp $self->{gunzip};
66 # Get all .pref file names
67 opendir my $fh, $self->{path_pref_en};
68 my @pref_files = grep { /\.pref$/ } readdir($fh);
70 $self->{pref_files} = \@pref_files;
72 # Get all available language codes
73 opendir $fh, $self->{path_po};
74 my @langs = map { ($_) =~ /(.*)-pref/ }
75 grep { $_ =~ /.*-pref/ } readdir($fh);
77 $self->{langs} = \@langs;
79 # Map for both interfaces opac/intranet
80 my $opachtdocs = $context->config('opachtdocs');
81 $self->{interface} = [
83 name => 'Intranet prog UI',
84 dir => $context->config('intrahtdocs') . '/prog',
85 suffix => '-staff-prog.po',
90 opendir my $dh, $context->config('opachtdocs');
91 for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
92 push @{$self->{interface}}, {
93 name => "OPAC $theme",
94 dir => "$opachtdocs/$theme",
95 suffix => "-opac-$theme.po",
99 # MARC flavours (hardcoded list)
100 for ( "MARC21", "UNIMARC", "NORMARC" ) {
101 # search for strings on staff & opac marc files
102 my $dirs = $context->config('intrahtdocs') . '/prog';
103 opendir $fh, $context->config('opachtdocs');
104 for ( grep { not /^\.|\.\.|lib$|xslt/ } readdir($fh) ) {
105 $dirs .= ' ' . "$opachtdocs/$_";
107 push @{$self->{interface}}, {
110 suffix => "-marc-$_.po",
114 # EN YAML installer files
115 push @{$self->{installer}}, {
116 name => "YAML installer files",
117 dirs => [ 'installer/data/mysql/en/mandatory',
118 'installer/data/mysql/en/optional'],
119 suffix => "-installer.po",
122 # EN MARC21 YAML installer files
123 push @{$self->{installer}}, {
124 name => "MARC21 YAML installer files",
125 dirs => [ 'installer/data/mysql/en/marcflavour/marc21/mandatory',
126 'installer/data/mysql/en/marcflavour/marc21/optional'],
127 suffix => "-installer-MARC21.po",
130 # EN UNIMARC YAML installer files
131 push @{$self->{installer}}, {
132 name => "UNIMARC YAML installer files",
133 dirs => [ 'installer/data/mysql/en/marcflavour/unimarc/mandatory', ],
134 suffix => "-installer-UNIMARC.po",
144 my $context = C4::Context->new;
145 my $trans_path = $Bin . '/po';
146 my $trans_file = "$trans_path/" . $self->{lang} . $suffix;
151 my ($self, $msgid, $default) = @_;
153 my $po = $self->{po}->{Locale::PO->quote($msgid)};
155 my $msgstr = Locale::PO->dequote($po->msgstr);
156 if ($msgstr and length($msgstr) > 0) {
164 sub get_translated_tab_content {
165 my ($self, $file, $tab_content) = @_;
167 if ( ref($tab_content) eq 'ARRAY' ) {
168 return $self->get_translated_prefs($file, $tab_content);
171 my $translated_tab_content = {
174 my $sysprefs = $tab_content->{$section};
175 my $msgid = sprintf('%s %s', $file, $section);
177 $self->get_trans_text($msgid, $section) => $self->get_translated_prefs($file, $sysprefs);
181 return $translated_tab_content;
184 sub get_translated_prefs {
185 my ($self, $file, $sysprefs) = @_;
187 my $translated_prefs = [
189 my ($pref_elt) = grep { ref($_) eq 'HASH' && exists $_->{pref} } @$_;
190 my $pref_name = $pref_elt ? $pref_elt->{pref} : '';
192 my $translated_syspref = [
194 $self->get_translated_pref($file, $pref_name, $_);
202 return $translated_prefs;
205 sub get_translated_pref {
206 my ($self, $file, $pref_name, $syspref) = @_;
208 unless (ref($syspref)) {
210 my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $syspref);
211 return $self->get_trans_text($msgid, $syspref);
214 my $translated_pref = {
217 my $value = $syspref->{$key};
219 my $translated_value = $value;
220 if (($key eq 'choices' || $key eq 'multiple') && ref($value) eq 'HASH') {
221 $translated_value = {
223 my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $value->{$_});
224 $_ => $self->get_trans_text($msgid, $value->{$_})
229 $key => $translated_value
233 return $translated_pref;
239 unless ( -r $self->{po_path_lang} ) {
240 print "Koha directories hierarchy for ", $self->{lang}, " must be created first\n";
244 $self->{po} = Locale::PO->load_file_ashash($self->po_filename("-pref.po"), 'utf8');
246 for my $file ( @{$self->{pref_files}} ) {
247 my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
249 my $translated_pref = {
252 my $tab_content = $pref->{$tab};
254 $self->get_trans_text($file, $tab) => $self->get_translated_tab_content($file, $tab_content);
259 my $file_trans = $self->{po_path_lang} . "/$file";
260 print "Write $file\n" if $self->{verbose};
261 DumpFile($file_trans, $translated_pref);
267 my ($self, $files) = @_;
268 say "Install templates" if $self->{verbose};
269 for my $trans ( @{$self->{interface}} ) {
270 my @t_dirs = split(" ", $trans->{dir});
271 for my $t_dir ( @t_dirs ) {
275 " Install templates '$trans->{name}'\n",
276 " From: $t_dir/en/\n",
277 " To : $t_dir/$self->{lang}\n",
278 " With: $self->{path_po}/$self->{lang}$trans->{suffix}\n"
281 my $trans_dir = "$t_dir/en/";
282 my $lang_dir = "$t_dir/$self->{lang}";
283 $lang_dir =~ s|/en/|/$self->{lang}/|;
284 mkdir $lang_dir unless -d $lang_dir;
285 # if installing MARC po file, only touch corresponding files
286 my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations
287 # if not installing MARC po file, ignore all MARC files
288 @nomarc = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
291 "$self->{process} install " .
294 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
296 ( @files ? ' -f ' . join ' -f ', @files : '') .
297 ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
307 my $po_file = $self->po_filename( $target->{suffix} );
308 return $srcyml unless ( -e $po_file );
310 my $po_ref = Locale::PO->load_file_ashash( $po_file );
312 my $dstyml = LoadFile( $srcyml );
314 # translate fields in table rows
315 my @tables = @{ $dstyml->{'tables'} };
316 for my $table ( @tables ) { # each table
317 my $table_name = ( keys %$table )[0];
318 my @translatable = @{ $table->{$table_name}->{translatable} };
319 my @rows = @{ $table->{$table_name}->{rows} };
320 my @multiline = @{ $table->{$table_name}->{'multiline'} }; # to check multiline values
321 for my $row ( @rows ) { # each row
322 for my $field ( @translatable ) { # each translatable field
323 if ( @multiline and grep { $_ eq $field } @multiline ) { # multiline fields, only notices ATM
324 foreach my $line ( @{$row->{$field}} ) {
325 next if ( $line =~ /^(\s*<.*?>\s*$|^\s*\[.*?\]\s*|\s*)$/ ); # discard pure html, TT, empty
327 while ( $line =~ s/(<<.*?>>|\[\%.*?\%\]|<.*?>)/\%s/ ) { # put placeholders, save matches
332 if ( $line =~ /^(\s|%s|-|[[:punct:]]|\(|\))*$/ ) { # ignore non strings
333 while ( @ttvar ) { # restore placeholders
334 my $var = shift @ttvar;
335 $line =~ s/\%s/$var/;
339 my $po = $po_ref->{"\"$line\""}; # quoted key
340 if ( $po and not defined( $po->fuzzy() ) # not fuzzy
341 and length( $po->msgid() ) > 2 # not empty msgid
342 and length( $po->msgstr() ) > 2 ) { # not empty msgstr
343 $line = $po->dequote( $po->msgstr() );
345 while ( @ttvar ) { # restore placeholders
346 my $var = shift @ttvar;
347 $line =~ s/\%s/$var/;
352 next unless defined $row->{$field}; # next if null value
353 my $po = $po_ref->{"\"$row->{$field}\""}; # quoted key
354 if ( $po and not defined( $po->fuzzy() ) # not fuzzy
355 and length( $po->msgid() ) > 2 # not empty msgid
356 and length( $po->msgstr() ) > 2 ) { # not empty msgstr
357 $row->{$field} = $po->dequote( $po->msgstr() );
364 # translate descriptions
365 for my $description ( @{ $dstyml->{'description'} } ) {
366 my $po = $po_ref->{"\"$description\""};
367 if ( $po and not defined( $po->fuzzy() )
368 and length( $po->msgid() ) > 2
369 and length( $po->msgstr() ) > 2 ) {
370 $description = $po->dequote( $po->msgstr() );
377 sub install_installer {
379 return unless ( $self->{installer} );
381 my $intradir = $self->{context}->config('intranetdir');
382 my $db_scheme = $self->{context}->config('db_scheme');
383 my $langdir = "$intradir/installer/data/$db_scheme/$self->{lang}";
385 say "$self->{lang} installer dir $langdir already exists.\nDelete it if you want to recreate it." if $self->{verbose};
389 say "Install installer files\n" if $self->{verbose};
391 for my $target ( @{ $self->{installer} } ) {
392 return unless ( -e $self->po_filename( $target->{suffix} ) );
393 for my $dir ( @{ $target->{dirs} } ) {
394 ( my $tdir = "$dir" ) =~ s|/en/|/$self->{lang}/|;
395 make_path("$intradir/$tdir");
397 opendir( my $dh, "$intradir/$dir" ) or die ("Can't open $intradir/$dir");
398 my @files = grep { ! /^\.+$/ } readdir($dh);
401 for my $file ( @files ) {
402 if ( $file =~ /yml$/ ) {
403 my $translated_yaml = translate_yaml( $self, $target, "$intradir/$dir/$file" );
404 open(my $fh, ">:encoding(UTF-8)", "$intradir/$tdir/$file");
405 DumpFile( $fh, $translated_yaml );
408 File::Copy::copy( "$intradir/$dir/$file", "$intradir/$tdir/$file" );
418 my ($language, $region, $country) = split /-/, $self->{lang};
419 $country //= $region;
420 my $locale = $language;
421 if ($country && length($country) == 2) {
422 $locale .= '_' . $country;
428 sub install_messages {
431 my $locale = $self->locale_name();
432 my $modir = "$self->{path_po}/$locale/LC_MESSAGES";
433 my $pofile = "$self->{path_po}/$self->{lang}-messages.po";
434 my $mofile = "$modir/$self->{domain}.mo";
435 my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po";
437 unless ( -f $pofile && -f $js_pofile ) {
438 die "PO files for language '$self->{lang}' do not exist";
441 say "Install messages ($locale)" if $self->{verbose};
443 system "$self->{msgfmt} -o $mofile $pofile";
445 my $js_locale_data = 'var json_locale_data = {"Koha":' . `$self->{po2json} $js_pofile` . '};';
446 my $progdir = $self->{context}->config('intrahtdocs') . '/prog';
447 mkdir "$progdir/$self->{lang}/js";
448 open my $fh, '>', "$progdir/$self->{lang}/js/locale_data.js";
449 print $fh $js_locale_data;
452 my $opachtdocs = $self->{context}->config('opachtdocs');
453 opendir(my $dh, $opachtdocs);
454 for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
455 mkdir "$opachtdocs/$theme/$self->{lang}/js";
456 open my $fh, '>', "$opachtdocs/$theme/$self->{lang}/js/locale_data.js";
457 print $fh $js_locale_data;
463 my ($self, $files) = @_;
464 my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
465 for my $lang ( @langs ) {
466 $self->set_lang( $lang );
467 opendir( my $dh, $self->{path_po} );
468 my @files = grep { $_ =~ /^$self->{lang}.*po$/ } readdir $dh;
469 foreach my $file ( @files ) {
470 say "Compress file $file" if $self->{verbose};
471 system "$self->{gzip} -9 $self->{path_po}/$file";
477 my ($self, $files) = @_;
478 my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
479 for my $lang ( @langs ) {
480 opendir( my $dh, $self->{path_po} );
481 $self->set_lang( $lang );
482 my @files = grep { $_ =~ /^$self->{lang}.*po.gz$/ } readdir $dh;
483 foreach my $file ( @files ) {
484 say "Uncompress file $file" if $self->{verbose};
485 system "$self->{gunzip} $self->{path_po}/$file";
491 my ($self, $files) = @_;
492 return unless $self->{lang};
495 if ($self->{pref_only}) {
496 $self->install_prefs();
498 $self->install_tmpl($files);
499 $self->install_prefs();
500 $self->install_messages();
501 $self->install_installer();
508 opendir( my $dh, $self->{path_po} );
509 my @files = grep { $_ =~ /-pref.(po|po.gz)$/ }
511 @files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files;
519 LangInstaller.pm - Handle templates and preferences translation
523 my $installer = LangInstaller->new( 'fr-FR' );
524 $installer->create();
525 $installer->update();
526 $installer->install();
527 for my $lang ( @{$installer->{langs} ) {
528 $installer->set_lang( $lan );
529 $installer->install();
536 Create a new instance of the installer object.
540 For the current language, create .po files for templates and preferences based
541 of the english ('en') version.
545 For the current language, update .po files.
549 For the current langage C<$self->{lang}, use .po files to translate the english
550 version of templates and preferences files and copy those files in the
551 appropriate directory.
555 =item translate create F<lang>
557 Create 4 kinds of .po files in F<po> subdirectory:
558 (1) one from each theme on opac pages templates,
559 (2) intranet templates,
561 (4) one for each MARC dialect.
566 =item F<lang>-opac-{theme}.po
568 Contains extracted text from english (en) OPAC templates found in
569 <KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
571 =item F<lang>-staff-prog.po
573 Contains extracted text from english (en) intranet templates found in
574 <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
576 =item F<lang>-pref.po
578 Contains extracted text from english (en) preferences. They are found in files
579 located in <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/admin/preferences
582 =item F<lang>-marc-{MARC}.po
584 Contains extracted text from english (en) files from opac and intranet,
585 related with MARC dialects.
589 =item pref-trans update F<lang>
591 Update .po files in F<po> directory, named F<lang>-*.po.
593 =item pref-trans install F<lang>