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( Dump LoadFile );
27 use FindBin qw( $Bin );
30 use File::Path qw( make_path );
33 use File::Temp qw( tempdir );
37 $YAML::Syck::ImplicitTyping = 1;
40 # Default file header for .po syspref files
41 my $default_pref_po_header = Locale::PO->new(-msgid => '', -msgstr =>
42 "Project-Id-Version: PACKAGE VERSION\\n" .
43 "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\n" .
44 "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n" .
45 "Language-Team: Koha Translate List <koha-translate\@lists.koha-community.org>\\n" .
46 "MIME-Version: 1.0\\n" .
47 "Content-Type: text/plain; charset=UTF-8\\n" .
48 "Content-Transfer-Encoding: 8bit\\n" .
49 "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
54 my ($self, $lang) = @_;
56 $self->{lang} = $lang;
57 $self->{po_path_lang} = $self->{context}->config('intrahtdocs') .
58 "/prog/$lang/modules/admin/preferences";
63 my ($class, $lang, $pref_only, $verbose) = @_;
67 my $context = C4::Context->new();
68 $self->{context} = $context;
69 $self->{path_pref_en} = $context->config('intrahtdocs') .
70 '/prog/en/modules/admin/preferences';
71 set_lang( $self, $lang ) if $lang;
72 $self->{pref_only} = $pref_only;
73 $self->{verbose} = $verbose;
74 $self->{process} = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
75 $self->{path_po} = "$Bin/po";
76 $self->{po} = { '' => $default_pref_po_header };
77 $self->{domain} = 'Koha';
78 $self->{cp} = `which cp`;
79 $self->{msgmerge} = `which msgmerge`;
80 $self->{msgfmt} = `which msgfmt`;
81 $self->{msginit} = `which msginit`;
82 $self->{xgettext} = `which xgettext`;
83 $self->{sed} = `which sed`;
85 chomp $self->{msgmerge};
86 chomp $self->{msgfmt};
87 chomp $self->{msginit};
88 chomp $self->{xgettext};
91 unless ($self->{xgettext}) {
92 die "Missing 'xgettext' executable. Have you installed the gettext package?\n";
95 # Get all .pref file names
96 opendir my $fh, $self->{path_pref_en};
97 my @pref_files = grep { /\.pref$/ } readdir($fh);
99 $self->{pref_files} = \@pref_files;
101 # Get all available language codes
102 opendir $fh, $self->{path_po};
103 my @langs = map { ($_) =~ /(.*)-pref/ }
104 grep { $_ =~ /.*-pref/ } readdir($fh);
106 $self->{langs} = \@langs;
108 # Map for both interfaces opac/intranet
109 my $opachtdocs = $context->config('opachtdocs');
110 $self->{interface} = [
112 name => 'Intranet prog UI',
113 dir => $context->config('intrahtdocs') . '/prog',
114 suffix => '-staff-prog.po',
119 opendir my $dh, $context->config('opachtdocs');
120 for my $theme ( grep { not /^\.|lib|xslt/ } readdir($dh) ) {
121 push @{$self->{interface}}, {
122 name => "OPAC $theme",
123 dir => "$opachtdocs/$theme",
124 suffix => "-opac-$theme.po",
128 # MARC flavours (hardcoded list)
129 for ( "MARC21", "UNIMARC", "NORMARC" ) {
130 # search for strings on staff & opac marc files
131 my $dirs = $context->config('intrahtdocs') . '/prog';
132 opendir $fh, $context->config('opachtdocs');
133 for ( grep { not /^\.|\.\.|lib$|xslt/ } readdir($fh) ) {
134 $dirs .= ' ' . "$opachtdocs/$_";
136 push @{$self->{interface}}, {
139 suffix => "-marc-$_.po",
150 my $context = C4::Context->new;
151 my $trans_path = $Bin . '/po';
152 my $trans_file = "$trans_path/" . $self->{lang} . "-pref.po";
158 my ($self, $id, $comment) = @_;
159 my $po = $self->{po};
162 $p->comment( $p->comment . "\n" . $comment );
165 $po->{$id} = Locale::PO->new(
166 -comment => $comment,
175 my ($self, $comment, $prefs) = @_;
177 for my $pref ( @$prefs ) {
179 for my $element ( @$pref ) {
180 if ( ref( $element) eq 'HASH' ) {
181 $pref_name = $element->{pref};
185 for my $element ( @$pref ) {
186 if ( ref( $element) eq 'HASH' ) {
187 while ( my ($key, $value) = each(%$element) ) {
188 next unless $key eq 'choices' or $key eq 'multiple';
189 next unless ref($value) eq 'HASH';
190 for my $ckey ( keys %$value ) {
191 my $id = $self->{file} . "#$pref_name# " . $value->{$ckey};
192 $self->po_append( $id, $comment );
197 $self->po_append( $self->{file} . "#$pref_name# $element", $comment );
205 my ($self, $id) = @_;
207 my $po = $self->{po}->{$id};
209 return Locale::PO->dequote($po->msgstr);
213 sub update_tab_prefs {
214 my ($self, $pref, $prefs) = @_;
216 for my $p ( @$prefs ) {
219 for my $element ( @$p ) {
220 if ( ref( $element) eq 'HASH' ) {
221 $pref_name = $element->{pref};
225 for my $i ( 0..@$p-1 ) {
226 my $element = $p->[$i];
227 if ( ref( $element) eq 'HASH' ) {
228 while ( my ($key, $value) = each(%$element) ) {
229 next unless $key eq 'choices' or $key eq 'multiple';
230 next unless ref($value) eq 'HASH';
231 for my $ckey ( keys %$value ) {
232 my $id = $self->{file} . "#$pref_name# " . $value->{$ckey};
233 my $text = $self->get_trans_text( $id );
234 $value->{$ckey} = $text if $text;
239 my $id = $self->{file} . "#$pref_name# $element";
240 my $text = $self->get_trans_text( $id );
241 $p->[$i] = $text if $text;
248 sub get_po_from_prefs {
251 for my $file ( @{$self->{pref_files}} ) {
252 my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
253 $self->{file} = $file;
254 # Entries for tab titles
255 $self->po_append( $self->{file}, $_ ) for keys %$pref;
256 while ( my ($tab, $tab_content) = each %$pref ) {
257 if ( ref($tab_content) eq 'ARRAY' ) {
258 $self->add_prefs( $tab, $tab_content );
261 while ( my ($section, $sysprefs) = each %$tab_content ) {
262 my $comment = "$tab > $section";
263 $self->po_append( $self->{file} . " " . $section, $comment );
264 $self->add_prefs( $comment, $sysprefs );
274 # Create file header if it doesn't already exist
275 my $po = $self->{po};
276 $po->{''} ||= $default_pref_po_header;
278 # Write .po entries into a file put in Koha standard po directory
279 Locale::PO->save_file_fromhash( $self->po_filename, $po );
280 say "Saved in file: ", $self->po_filename if $self->{verbose};
284 sub get_po_merged_with_en {
287 # Get po from current 'en' .pref files
288 $self->get_po_from_prefs();
289 my $po_current = $self->{po};
291 # Get po from previous generation
292 my $po_previous = Locale::PO->load_file_ashash( $self->po_filename );
294 for my $id ( keys %$po_current ) {
295 my $po = $po_previous->{Locale::PO->quote($id)};
297 my $text = Locale::PO->dequote( $po->msgstr );
298 $po_current->{$id}->msgstr( $text );
305 print "Update '", $self->{lang},
306 "' preferences .po file from 'en' .pref files\n" if $self->{verbose};
307 $self->get_po_merged_with_en();
315 unless ( -r $self->{po_path_lang} ) {
316 print "Koha directories hierarchy for ", $self->{lang}, " must be created first\n";
320 # Get the language .po file merged with last modified 'en' preferences
321 $self->get_po_merged_with_en();
323 for my $file ( @{$self->{pref_files}} ) {
324 my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
325 $self->{file} = $file;
326 # First, keys are replaced (tab titles)
329 $self->get_trans_text( $self->{file} ) || $_ => $pref->{$_}
333 while ( my ($tab, $tab_content) = each %$pref ) {
334 if ( ref($tab_content) eq 'ARRAY' ) {
335 $self->update_tab_prefs( $pref, $tab_content );
338 while ( my ($section, $sysprefs) = each %$tab_content ) {
339 $self->update_tab_prefs( $pref, $sysprefs );
342 for my $section ( keys %$tab_content ) {
343 my $id = $self->{file} . " $section";
344 my $text = $self->get_trans_text($id);
345 my $nsection = $text ? $text : $section;
346 if( exists $ntab->{$nsection} ) {
347 # When translations collide (see BZ 18634)
348 push @{$ntab->{$nsection}}, @{$tab_content->{$section}};
350 $ntab->{$nsection} = $tab_content->{$section};
353 $pref->{$tab} = $ntab;
355 my $file_trans = $self->{po_path_lang} . "/$file";
356 print "Write $file\n" if $self->{verbose};
357 open my $fh, ">", $file_trans;
358 print $fh Dump($pref);
364 my ($self, $files) = @_;
365 say "Install templates" if $self->{verbose};
366 for my $trans ( @{$self->{interface}} ) {
367 my @t_dirs = split(" ", $trans->{dir});
368 for my $t_dir ( @t_dirs ) {
372 " Install templates '$trans->{name}'\n",
373 " From: $t_dir/en/\n",
374 " To : $t_dir/$self->{lang}\n",
375 " With: $self->{path_po}/$self->{lang}$trans->{suffix}\n"
378 my $trans_dir = "$t_dir/en/";
379 my $lang_dir = "$t_dir/$self->{lang}";
380 $lang_dir =~ s|/en/|/$self->{lang}/|;
381 mkdir $lang_dir unless -d $lang_dir;
382 # if installing MARC po file, only touch corresponding files
383 my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations
384 # if not installing MARC po file, ignore all MARC files
385 @nomarc = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
388 "$self->{process} install " .
391 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
393 ( @files ? ' -f ' . join ' -f ', @files : '') .
394 ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
401 my ($self, $files) = @_;
403 say "Update templates" if $self->{verbose};
404 for my $trans ( @{$self->{interface}} ) {
408 " Update templates '$trans->{name}'\n",
409 " From: $trans->{dir}/en/\n",
410 " To : $self->{path_po}/$self->{lang}$trans->{suffix}\n"
413 my $trans_dir = join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs
414 # if processing MARC po file, only use corresponding files
415 my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations
416 # if not processing MARC po file, ignore all MARC files
417 @nomarc = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
420 "$self->{process} update " .
422 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
424 ( @files ? ' -f ' . join ' -f ', @files : '') .
425 ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
433 if ( -e $self->po_filename ) {
434 say "Preferences .po file already exists. Delete it if you want to recreate it.";
437 $self->get_po_from_prefs();
443 my ($self, $files) = @_;
445 say "Create templates\n" if $self->{verbose};
446 for my $trans ( @{$self->{interface}} ) {
450 " Create templates .po files for '$trans->{name}'\n",
451 " From: $trans->{dir}/en/\n",
452 " To : $self->{path_po}/$self->{lang}$trans->{suffix}\n"
455 my $trans_dir = join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs
456 # if processing MARC po file, only use corresponding files
457 my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations
458 # if not processing MARC po file, ignore all MARC files
459 @nomarc = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
462 "$self->{process} create " .
464 "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
466 ( @files ? ' -f ' . join ' -f ', @files : '') .
467 ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
474 my ($language, $region, $country) = split /-/, $self->{lang};
475 $country //= $region;
476 my $locale = $language;
477 if ($country && length($country) == 2) {
478 $locale .= '_' . $country;
484 sub create_messages {
487 my $pot = "$Bin/$self->{domain}.pot";
488 my $po = "$self->{path_po}/$self->{lang}-messages.po";
491 $self->extract_messages();
494 say "Create messages ($self->{lang})" if $self->{verbose};
495 my $locale = $self->locale_name();
496 system "$self->{msginit} -i $pot -o $po -l $locale --no-translator 2> /dev/null";
497 warn "Problems creating $pot ".$? if ( $? == -1 );
499 # If msginit failed to correctly set Plural-Forms, set a default one
500 system "$self->{sed} --in-place $po "
501 . "--expression='s/Plural-Forms: nplurals=INTEGER; plural=EXPRESSION/Plural-Forms: nplurals=2; plural=(n != 1)/'";
504 sub update_messages {
507 my $pot = "$Bin/$self->{domain}.pot";
508 my $po = "$self->{path_po}/$self->{lang}-messages.po";
511 $self->extract_messages();
515 say "Update messages ($self->{lang})" if $self->{verbose};
516 system "$self->{msgmerge} --backup=off --quiet -U $po $pot";
518 $self->create_messages();
522 sub extract_messages_from_templates {
523 my ($self, $tempdir, $type, @files) = @_;
525 my $htdocs = $type eq 'intranet' ? 'intrahtdocs' : 'opachtdocs';
526 my $dir = $self->{context}->config($htdocs);
527 my @keywords = qw(t tx tn txn tnx tp tpx tnp tnpx);
528 my $parser = Template::Parser->new();
530 foreach my $file (@files) {
531 say "Extract messages from $file" if $self->{verbose};
532 my $template = read_file(File::Spec->catfile($dir, $file));
534 # No need to process a file that doesn't use the i18n.inc file.
535 next unless $template =~ /i18n\.inc/;
537 my $data = $parser->parse($template);
539 warn "Error at $file : " . $parser->error();
543 my $destfile = $type eq 'intranet' ?
544 File::Spec->catfile($tempdir, 'koha-tmpl', 'intranet-tmpl', $file) :
545 File::Spec->catfile($tempdir, 'koha-tmpl', 'opac-tmpl', $file);
547 make_path(dirname($destfile));
548 open my $fh, '>', $destfile;
550 my @blocks = ($data->{BLOCK}, values %{ $data->{DEFBLOCKS} });
551 foreach my $block (@blocks) {
552 my $document = PPI::Document->new(\$block);
554 # [% t('foo') %] is compiled to
555 # $output .= $stash->get(['t', ['foo']]);
556 # We try to find all nodes corresponding to keyword (here 't')
557 my $nodes = $document->find(sub {
558 my ($topnode, $element) = @_;
560 # Filter out non-valid keywords
561 return 0 unless ($element->isa('PPI::Token::Quote::Single'));
562 return 0 unless (grep {$element->content eq qq{'$_'}} @keywords);
564 # keyword (e.g. 't') should be the first element of the arrayref
565 # passed to $stash->get()
566 return 0 if $element->sprevious_sibling;
568 return 0 unless $element->snext_sibling
569 && $element->snext_sibling->snext_sibling
570 && $element->snext_sibling->snext_sibling->isa('PPI::Structure::Constructor');
572 # Check that it's indeed a call to $stash->get()
573 my $statement = $element->statement->parent->statement->parent->statement;
574 return 0 unless grep { $_->isa('PPI::Token::Symbol') && $_->content eq '$stash' } $statement->children;
575 return 0 unless grep { $_->isa('PPI::Token::Operator') && $_->content eq '->' } $statement->children;
576 return 0 unless grep { $_->isa('PPI::Token::Word') && $_->content eq 'get' } $statement->children;
583 # Write the Perl equivalent of calls to t* functions family, so
584 # xgettext can extract the strings correctly
585 foreach my $node (@$nodes) {
587 $_->significant && !$_->isa('PPI::Token::Operator') ? $_->content : ()
588 } $node->snext_sibling->snext_sibling->find_first('PPI::Statement')->children;
590 my $keyword = $node->content;
591 $keyword =~ s/^'t(.*)'$/__$1/;
593 # Only keep required args to have a clean output
594 my @required_args = shift @args;
595 push @required_args, shift @args if $keyword =~ /n/;
596 push @required_args, shift @args if $keyword =~ /p/;
598 say $fh "$keyword(" . join(', ', @required_args) . ");";
609 sub extract_messages {
612 say "Extract messages into POT file" if $self->{verbose};
614 my $intranetdir = $self->{context}->config('intranetdir');
615 my $opacdir = $self->{context}->config('opacdir');
617 # Find common ancestor directory
618 my @intranetdirs = File::Spec->splitdir($intranetdir);
619 my @opacdirs = File::Spec->splitdir($opacdir);
621 while (@intranetdirs and @opacdirs) {
622 my ($dir1, $dir2) = (shift @intranetdirs, shift @opacdirs);
623 last if $dir1 ne $dir2;
624 push @basedirs, $dir1;
626 my $basedir = File::Spec->catdir(@basedirs);
629 my @directories_to_scan = ('.');
630 my @blacklist = map { File::Spec->catdir(@intranetdirs, $_) } qw(blib koha-tmpl skel tmp t);
631 while (@directories_to_scan) {
632 my $dir = shift @directories_to_scan;
633 opendir DIR, File::Spec->catdir($basedir, $dir) or die "Unable to open $dir: $!";
634 foreach my $entry (readdir DIR) {
635 next if $entry =~ /^\./;
636 my $relentry = File::Spec->catfile($dir, $entry);
637 my $abspath = File::Spec->catfile($basedir, $relentry);
638 if (-d $abspath and not grep /^$relentry$/, @blacklist) {
639 push @directories_to_scan, $relentry;
640 } elsif (-f $abspath and $relentry =~ /\.(pl|pm)$/) {
641 push @files_to_scan, $relentry;
646 my $intrahtdocs = $self->{context}->config('intrahtdocs');
647 my $opachtdocs = $self->{context}->config('opachtdocs');
649 my @intranet_tt_files;
651 if ($File::Find::dir =~ m|/en/| && $_ =~ m/\.(tt|inc)$/) {
652 my $filename = $File::Find::name;
653 $filename =~ s|^$intrahtdocs/||;
654 push @intranet_tt_files, $filename;
660 if ($File::Find::dir =~ m|/en/| && $_ =~ m/\.(tt|inc)$/) {
661 my $filename = $File::Find::name;
662 $filename =~ s|^$opachtdocs/||;
663 push @opac_tt_files, $filename;
667 my $tempdir = tempdir('Koha-translate-XXXX', TMPDIR => 1, CLEANUP => 1);
668 $self->extract_messages_from_templates($tempdir, 'intranet', @intranet_tt_files);
669 $self->extract_messages_from_templates($tempdir, 'opac', @opac_tt_files);
671 @intranet_tt_files = map { File::Spec->catfile('koha-tmpl', 'intranet-tmpl', $_) } @intranet_tt_files;
672 @opac_tt_files = map { File::Spec->catfile('koha-tmpl', 'opac-tmpl', $_) } @opac_tt_files;
673 my @tt_files = grep { -e File::Spec->catfile($tempdir, $_) } @intranet_tt_files, @opac_tt_files;
675 push @files_to_scan, @tt_files;
677 my $xgettext_cmd = "$self->{xgettext} --force-po -L Perl --from-code=UTF-8 "
678 . "--package-name=Koha --package-version='' "
679 . "-k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 -k__p:1c,2 "
680 . "-k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ -kN__n:1,2 "
681 . "-kN__p:1c,2 -kN__np:1c,2,3 "
682 . "-o $Bin/$self->{domain}.pot -D $tempdir -D $basedir";
683 $xgettext_cmd .= " $_" foreach (@files_to_scan);
685 if (system($xgettext_cmd) != 0) {
686 die "system call failed: $xgettext_cmd";
689 my $replace_charset_cmd = "$self->{sed} --in-place " .
690 "$Bin/$self->{domain}.pot " .
691 "--expression='s/charset=CHARSET/charset=UTF-8/'";
692 if (system($replace_charset_cmd) != 0) {
693 die "system call failed: $replace_charset_cmd";
697 sub install_messages {
700 my $locale = $self->locale_name();
701 my $modir = "$self->{path_po}/$locale/LC_MESSAGES";
702 my $pofile = "$self->{path_po}/$self->{lang}-messages.po";
703 my $mofile = "$modir/$self->{domain}.mo";
705 if ( not -f $pofile ) {
706 $self->create_messages();
708 say "Install messages ($locale)" if $self->{verbose};
710 system "$self->{msgfmt} -o $mofile $pofile";
716 unlink "$Bin/$self->{domain}.pot";
720 my ($self, $files) = @_;
721 return unless $self->{lang};
722 $self->install_tmpl($files) unless $self->{pref_only};
723 $self->install_prefs();
724 $self->install_messages();
731 opendir( my $dh, $self->{path_po} );
732 my @files = grep { $_ =~ /-pref.po$/ }
734 @files = map { $_ =~ s/-pref.po$//; $_ } @files;
739 my ($self, $files) = @_;
740 my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
741 for my $lang ( @langs ) {
742 $self->set_lang( $lang );
743 $self->update_tmpl($files) unless $self->{pref_only};
744 $self->update_prefs();
745 $self->update_messages();
752 my ($self, $files) = @_;
753 return unless $self->{lang};
754 $self->create_tmpl($files) unless $self->{pref_only};
755 $self->create_prefs();
756 $self->create_messages();
767 LangInstaller.pm - Handle templates and preferences translation
771 my $installer = LangInstaller->new( 'fr-FR' );
772 $installer->create();
773 $installer->update();
774 $installer->install();
775 for my $lang ( @{$installer->{langs} ) {
776 $installer->set_lang( $lan );
777 $installer->install();
784 Create a new instance of the installer object.
788 For the current language, create .po files for templates and preferences based
789 of the english ('en') version.
793 For the current language, update .po files.
797 For the current langage C<$self->{lang}, use .po files to translate the english
798 version of templates and preferences files and copy those files in the
799 appropriate directory.
803 =item translate create F<lang>
805 Create 4 kinds of .po files in F<po> subdirectory:
806 (1) one from each theme on opac pages templates,
807 (2) intranet templates,
809 (4) one for each MARC dialect.
814 =item F<lang>-opac-{theme}.po
816 Contains extracted text from english (en) OPAC templates found in
817 <KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
819 =item F<lang>-staff-prog.po
821 Contains extracted text from english (en) intranet templates found in
822 <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
824 =item F<lang>-pref.po
826 Contains extracted text from english (en) preferences. They are found in files
827 located in <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/admin/preferences
830 =item F<lang>-marc-{MARC}.po
832 Contains extracted text from english (en) files from opac and intranet,
833 related with MARC dialects.
837 =item pref-trans update F<lang>
839 Update .po files in F<po> directory, named F<lang>-*.po.
841 =item pref-trans install F<lang>