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 suffix => "-installer-UNIMARC.po",
140 my $trans_path = $Bin . '/po';
141 my $trans_file = "$trans_path/" . $self->{lang} . $suffix;
146 my ($self, $msgid, $default) = @_;
148 my $po = $self->{po}->{Locale::PO->quote($msgid)};
150 my $msgstr = Locale::PO->dequote($po->msgstr);
151 if ($msgstr and length($msgstr) > 0) {
159 sub get_translated_tab_content {
160 my ($self, $file, $tab_content) = @_;
162 if ( ref($tab_content) eq 'ARRAY' ) {
163 return $self->get_translated_prefs($file, $tab_content);
166 my $translated_tab_content = {
169 my $sysprefs = $tab_content->{$section};
170 my $msgid = sprintf('%s %s', $file, $section);
172 $self->get_trans_text($msgid, $section) => $self->get_translated_prefs($file, $sysprefs);
176 return $translated_tab_content;
179 sub get_translated_prefs {
180 my ($self, $file, $sysprefs) = @_;
182 my $translated_prefs = [
184 my ($pref_elt) = grep { ref($_) eq 'HASH' && exists $_->{pref} } @$_;
185 my $pref_name = $pref_elt ? $pref_elt->{pref} : '';
187 my $translated_syspref = [
189 $self->get_translated_pref($file, $pref_name, $_);
197 return $translated_prefs;
200 sub get_translated_pref {
201 my ($self, $file, $pref_name, $syspref) = @_;
203 unless (ref($syspref)) {
205 my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $syspref);
206 return $self->get_trans_text($msgid, $syspref);
209 my $translated_pref = {
212 my $value = $syspref->{$key};
214 my $translated_value = $value;
215 if (($key eq 'choices' || $key eq 'multiple') && ref($value) eq 'HASH') {
216 $translated_value = {
218 my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $value->{$_});
219 $_ => $self->get_trans_text($msgid, $value->{$_})
224 $key => $translated_value
228 return $translated_pref;
234 unless ( -r $self->{po_path_lang} ) {
235 print "Koha directories hierarchy for ", $self->{lang}, " must be created first\n";
239 $self->{po} = Locale::PO->load_file_ashash($self->po_filename("-pref.po"), 'utf8');
241 for my $file ( @{$self->{pref_files}} ) {
242 my $pref = YAML::XS::LoadFile( $self->{path_pref_en} . "/$file" );
244 my $translated_pref = {
247 my $tab_content = $pref->{$tab};
249 $self->get_trans_text($file, $tab) => $self->get_translated_tab_content($file, $tab_content);
254 my $file_trans = $self->{po_path_lang} . "/$file";
255 print "Write $file\n" if $self->{verbose};
256 YAML::XS::DumpFile($file_trans, $translated_pref);
262 my ($self, $files) = @_;
263 say "Install templates" if $self->{verbose};
264 for my $trans ( @{$self->{interface}} ) {
265 my @t_dirs = split(" ", $trans->{dir});
266 for my $t_dir ( @t_dirs ) {
270 " Install templates '$trans->{name}'\n",
271 " From: $t_dir/en/\n",
272 " To : $t_dir/$self->{lang}\n",
273 " With: $self->{path_po}/$self->{lang}$trans->{suffix}\n"
276 my $trans_dir = "$t_dir/en/";
277 my $lang_dir = "$t_dir/$self->{lang}";
278 $lang_dir =~ s|/en/|/$self->{lang}/|;
279 mkdir $lang_dir unless -d $lang_dir;
280 # if installing MARC po file, only touch corresponding files
281 my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations
282 # if not installing MARC po file, ignore all MARC files
283 @nomarc = ( 'marc21', 'unimarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
286 "$self->{process} install " .
289 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
291 ( @files ? ' -f ' . join ' -f ', @files : '') .
292 ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
302 my $po_file = $self->po_filename( $target->{suffix} );
303 return $srcyml unless ( -e $po_file );
305 my $po_ref = Locale::PO->load_file_ashash( $po_file, 'utf8' );
307 my $dstyml = YAML::XS::LoadFile( $srcyml );
309 # translate fields in table rows
310 my @tables = @{ $dstyml->{'tables'} };
311 for my $table ( @tables ) { # each table
312 my $table_name = ( keys %$table )[0];
313 my @translatable = @{ $table->{$table_name}->{translatable} };
314 my @rows = @{ $table->{$table_name}->{rows} };
315 my @multiline = @{ $table->{$table_name}->{'multiline'} }; # to check multiline values
316 for my $row ( @rows ) { # each row
317 for my $field ( @translatable ) { # each translatable field
318 if ( @multiline and grep { $_ eq $field } @multiline ) { # multiline fields, only notices ATM
319 foreach my $line ( @{$row->{$field}} ) {
320 next if ( $line =~ /^(\s*<.*?>\s*$|^\s*\[.*?\]\s*|\s*)$/ ); # discard pure html, TT, empty
322 while ( $line =~ s/(<<.*?>>|\[\%.*?\%\]|<.*?>)/\%s/ ) { # put placeholders, save matches
327 if ( $line =~ /^(\s|%s|-|[[:punct:]]|\(|\))*$/ ) { # ignore non strings
328 while ( @ttvar ) { # restore placeholders
329 my $var = shift @ttvar;
330 $line =~ s/\%s/$var/;
334 my $po = $po_ref->{"\"$line\""}; # quoted key
335 if ( $po and not defined( $po->fuzzy() ) # not fuzzy
336 and length( $po->msgid() ) > 2 # not empty msgid
337 and length( $po->msgstr() ) > 2 ) { # not empty msgstr
338 $line = $po->dequote( $po->msgstr() );
340 while ( @ttvar ) { # restore placeholders
341 my $var = shift @ttvar;
342 $line =~ s/\%s/$var/;
347 next unless defined $row->{$field}; # next if null value
348 my $po = $po_ref->{"\"$row->{$field}\""}; # quoted key
349 if ( $po and not defined( $po->fuzzy() ) # not fuzzy
350 and length( $po->msgid() ) > 2 # not empty msgid
351 and length( $po->msgstr() ) > 2 ) { # not empty msgstr
352 $row->{$field} = $po->dequote( $po->msgstr() );
359 # translate descriptions
360 for my $description ( @{ $dstyml->{'description'} } ) {
361 my $po = $po_ref->{"\"$description\""};
362 if ( $po and not defined( $po->fuzzy() )
363 and length( $po->msgid() ) > 2
364 and length( $po->msgstr() ) > 2 ) {
365 $description = $po->dequote( $po->msgstr() );
372 sub install_installer {
374 return unless ( $self->{installer} );
376 my $intradir = C4::Context->config('intranetdir');
377 my $db_scheme = C4::Context->config('db_scheme');
378 my $langdir = "$intradir/installer/data/$db_scheme/$self->{lang}";
380 say "$self->{lang} installer dir $langdir already exists.\nDelete it if you want to recreate it." if $self->{verbose};
384 say "Install installer files\n" if $self->{verbose};
386 for my $target ( @{ $self->{installer} } ) {
387 return unless ( -e $self->po_filename( $target->{suffix} ) );
388 for my $dir ( @{ $target->{dirs} } ) {
389 ( my $tdir = "$dir" ) =~ s|/en/|/$self->{lang}/|;
390 make_path("$intradir/$tdir");
392 opendir( my $dh, "$intradir/$dir" ) or die ("Can't open $intradir/$dir");
393 my @files = grep { ! /^\.+$/ } readdir($dh);
396 for my $file ( @files ) {
397 if ( $file =~ /yml$/ ) {
398 my $translated_yaml = translate_yaml( $self, $target, "$intradir/$dir/$file" );
399 YAML::XS::DumpFile( "$intradir/$tdir/$file", $translated_yaml );
401 File::Copy::copy( "$intradir/$dir/$file", "$intradir/$tdir/$file" );
411 my ($language, $region, $country) = split /-/, $self->{lang};
412 $country //= $region;
413 my $locale = $language;
414 if ($country && length($country) == 2) {
415 $locale .= '_' . $country;
421 sub install_messages {
424 my $locale = $self->locale_name();
425 my $modir = "$self->{path_po}/$locale/LC_MESSAGES";
426 my $pofile = "$self->{path_po}/$self->{lang}-messages.po";
427 my $mofile = "$modir/$self->{domain}.mo";
428 my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po";
430 unless ( -f $pofile && -f $js_pofile ) {
431 die "PO files for language '$self->{lang}' do not exist";
434 say "Install messages ($locale)" if $self->{verbose};
436 system "$self->{msgfmt} -o $mofile $pofile";
438 my $js_locale_data = 'var json_locale_data = {"Koha":' . `$self->{po2json} $js_pofile` . '};';
439 my $progdir = C4::Context->config('intrahtdocs') . '/prog';
440 mkdir "$progdir/$self->{lang}/js";
441 open my $fh, '>', "$progdir/$self->{lang}/js/locale_data.js";
442 print $fh $js_locale_data;
445 my $opachtdocs = C4::Context->config('opachtdocs');
446 opendir(my $dh, $opachtdocs);
447 for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
448 mkdir "$opachtdocs/$theme/$self->{lang}/js";
449 open my $fh, '>', "$opachtdocs/$theme/$self->{lang}/js/locale_data.js";
450 print $fh $js_locale_data;
456 my ($self, $files) = @_;
457 my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
458 for my $lang ( @langs ) {
459 $self->set_lang( $lang );
460 opendir( my $dh, $self->{path_po} );
461 my @files = grep { $_ =~ /^$self->{lang}.*po$/ } readdir $dh;
462 foreach my $file ( @files ) {
463 say "Compress file $file" if $self->{verbose};
464 system "$self->{gzip} -9 $self->{path_po}/$file";
470 my ($self, $files) = @_;
471 my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
472 for my $lang ( @langs ) {
473 opendir( my $dh, $self->{path_po} );
474 $self->set_lang( $lang );
475 my @files = grep { $_ =~ /^$self->{lang}.*po.gz$/ } readdir $dh;
476 foreach my $file ( @files ) {
477 say "Uncompress file $file" if $self->{verbose};
478 system "$self->{gunzip} $self->{path_po}/$file";
484 my ($self, $files) = @_;
485 return unless $self->{lang};
488 if ($self->{pref_only}) {
489 $self->install_prefs();
491 $self->install_tmpl($files);
492 $self->install_prefs();
493 $self->install_messages();
494 $self->install_installer();
501 opendir( my $dh, $self->{path_po} );
502 my @files = grep { $_ =~ /-pref.(po|po.gz)$/ }
504 @files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files;
512 LangInstaller.pm - Handle templates and preferences translation
516 my $installer = LangInstaller->new( 'fr-FR' );
517 $installer->create();
518 $installer->update();
519 $installer->install();
520 for my $lang ( @{$installer->{langs} ) {
521 $installer->set_lang( $lan );
522 $installer->install();
529 Create a new instance of the installer object.
533 For the current language, create .po files for templates and preferences based
534 of the english ('en') version.
538 For the current language, update .po files.
542 For the current langage C<$self->{lang}, use .po files to translate the english
543 version of templates and preferences files and copy those files in the
544 appropriate directory.
548 =item translate create F<lang>
550 Create 4 kinds of .po files in F<po> subdirectory:
551 (1) one from each theme on opac pages templates,
552 (2) intranet templates,
554 (4) one for each MARC dialect.
559 =item F<lang>-opac-{theme}.po
561 Contains extracted text from english (en) OPAC templates found in
562 <KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
564 =item F<lang>-staff-prog.po
566 Contains extracted text from english (en) intranet templates found in
567 <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
569 =item F<lang>-pref.po
571 Contains extracted text from english (en) preferences. They are found in files
572 located in <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/admin/preferences
575 =item F<lang>-marc-{MARC}.po
577 Contains extracted text from english (en) files from opac and intranet,
578 related with MARC dialects.
582 =item pref-trans update F<lang>
584 Update .po files in F<po> directory, named F<lang>-*.po.
586 =item pref-trans install F<lang>