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';
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';
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";
498 # If msginit failed to correctly set Plural-Forms, set a default one
499 system "$self->{sed} --in-place $po "
500 . "--expression='s/Plural-Forms: nplurals=INTEGER; plural=EXPRESSION/Plural-Forms: nplurals=2; plural=(n != 1)/'";
503 sub update_messages
{
506 my $pot = "$Bin/$self->{domain}.pot";
507 my $po = "$self->{path_po}/$self->{lang}-messages.po";
510 $self->extract_messages();
514 say "Update messages ($self->{lang})" if $self->{verbose
};
515 system "$self->{msgmerge} --quiet -U $po $pot";
517 $self->create_messages();
521 sub extract_messages_from_templates
{
522 my ($self, $tempdir, $type, @files) = @_;
524 my $htdocs = $type eq 'intranet' ?
'intrahtdocs' : 'opachtdocs';
525 my $dir = $self->{context
}->config($htdocs);
526 my @keywords = qw(t tx tn txn tnx tp tpx tnp tnpx);
527 my $parser = Template
::Parser
->new();
529 foreach my $file (@files) {
530 say "Extract messages from $file" if $self->{verbose
};
531 my $template = read_file
(File
::Spec
->catfile($dir, $file));
533 # No need to process a file that doesn't use the i18n.inc file.
534 next unless $template =~ /i18n\.inc/;
536 my $data = $parser->parse($template);
538 warn "Error at $file : " . $parser->error();
542 my $destfile = $type eq 'intranet' ?
543 File
::Spec
->catfile($tempdir, 'koha-tmpl', 'intranet-tmpl', $file) :
544 File
::Spec
->catfile($tempdir, 'koha-tmpl', 'opac-tmpl', $file);
546 make_path
(dirname
($destfile));
547 open my $fh, '>', $destfile;
549 my @blocks = ($data->{BLOCK
}, values %{ $data->{DEFBLOCKS
} });
550 foreach my $block (@blocks) {
551 my $document = PPI
::Document
->new(\
$block);
553 # [% t('foo') %] is compiled to
554 # $output .= $stash->get(['t', ['foo']]);
555 # We try to find all nodes corresponding to keyword (here 't')
556 my $nodes = $document->find(sub {
557 my ($topnode, $element) = @_;
559 # Filter out non-valid keywords
560 return 0 unless ($element->isa('PPI::Token::Quote::Single'));
561 return 0 unless (grep {$element->content eq qq{'$_'}} @keywords);
563 # keyword (e.g. 't') should be the first element of the arrayref
564 # passed to $stash->get()
565 return 0 if $element->sprevious_sibling;
567 return 0 unless $element->snext_sibling
568 && $element->snext_sibling->snext_sibling
569 && $element->snext_sibling->snext_sibling->isa('PPI::Structure::Constructor');
571 # Check that it's indeed a call to $stash->get()
572 my $statement = $element->statement->parent->statement->parent->statement;
573 return 0 unless grep { $_->isa('PPI::Token::Symbol') && $_->content eq '$stash' } $statement->children;
574 return 0 unless grep { $_->isa('PPI::Token::Operator') && $_->content eq '->' } $statement->children;
575 return 0 unless grep { $_->isa('PPI::Token::Word') && $_->content eq 'get' } $statement->children;
582 # Write the Perl equivalent of calls to t* functions family, so
583 # xgettext can extract the strings correctly
584 foreach my $node (@
$nodes) {
586 $_->significant && !$_->isa('PPI::Token::Operator') ?
$_->content : ()
587 } $node->snext_sibling->snext_sibling->find_first('PPI::Statement')->children;
589 my $keyword = $node->content;
590 $keyword =~ s/^'t(.*)'$/__$1/;
592 # Only keep required args to have a clean output
593 my @required_args = shift @args;
594 push @required_args, shift @args if $keyword =~ /n/;
595 push @required_args, shift @args if $keyword =~ /p/;
597 say $fh "$keyword(" . join(', ', @required_args) . ");";
608 sub extract_messages
{
611 say "Extract messages into POT file" if $self->{verbose
};
613 my $intranetdir = $self->{context
}->config('intranetdir');
614 my $opacdir = $self->{context
}->config('opacdir');
616 # Find common ancestor directory
617 my @intranetdirs = File
::Spec
->splitdir($intranetdir);
618 my @opacdirs = File
::Spec
->splitdir($opacdir);
620 while (@intranetdirs and @opacdirs) {
621 my ($dir1, $dir2) = (shift @intranetdirs, shift @opacdirs);
622 last if $dir1 ne $dir2;
623 push @basedirs, $dir1;
625 my $basedir = File
::Spec
->catdir(@basedirs);
628 my @directories_to_scan = ('.');
629 my @blacklist = map { File
::Spec
->catdir(@intranetdirs, $_) } qw(blib koha-tmpl skel tmp t);
630 while (@directories_to_scan) {
631 my $dir = shift @directories_to_scan;
632 opendir DIR
, File
::Spec
->catdir($basedir, $dir) or die "Unable to open $dir: $!";
633 foreach my $entry (readdir DIR
) {
634 next if $entry =~ /^\./;
635 my $relentry = File
::Spec
->catfile($dir, $entry);
636 my $abspath = File
::Spec
->catfile($basedir, $relentry);
637 if (-d
$abspath and not grep /^$relentry$/, @blacklist) {
638 push @directories_to_scan, $relentry;
639 } elsif (-f
$abspath and $relentry =~ /\.(pl|pm)$/) {
640 push @files_to_scan, $relentry;
645 my $intrahtdocs = $self->{context
}->config('intrahtdocs');
646 my $opachtdocs = $self->{context
}->config('opachtdocs');
648 my @intranet_tt_files;
650 if ($File::Find
::dir
=~ m
|/en/| && $_ =~ m/\.(tt|inc)$/) {
651 my $filename = $File::Find
::name
;
652 $filename =~ s
|^$intrahtdocs/||;
653 push @intranet_tt_files, $filename;
659 if ($File::Find
::dir
=~ m
|/en/| && $_ =~ m/\.(tt|inc)$/) {
660 my $filename = $File::Find
::name
;
661 $filename =~ s
|^$opachtdocs/||;
662 push @opac_tt_files, $filename;
666 my $tempdir = tempdir
('Koha-translate-XXXX', TMPDIR
=> 1, CLEANUP
=> 1);
667 $self->extract_messages_from_templates($tempdir, 'intranet', @intranet_tt_files);
668 $self->extract_messages_from_templates($tempdir, 'opac', @opac_tt_files);
670 @intranet_tt_files = map { File
::Spec
->catfile('koha-tmpl', 'intranet-tmpl', $_) } @intranet_tt_files;
671 @opac_tt_files = map { File
::Spec
->catfile('koha-tmpl', 'opac-tmpl', $_) } @opac_tt_files;
672 my @tt_files = grep { -e File
::Spec
->catfile($tempdir, $_) } @intranet_tt_files, @opac_tt_files;
674 push @files_to_scan, @tt_files;
676 my $xgettext_cmd = "$self->{xgettext} --force-po -L Perl --from-code=UTF-8 "
677 . "--package-name=Koha --package-version='' "
678 . "-k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 -k__p:1c,2 "
679 . "-k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ -kN__n:1,2 "
680 . "-kN__p:1c,2 -kN__np:1c,2,3 "
681 . "-o $Bin/$self->{domain}.pot -D $tempdir -D $basedir";
682 $xgettext_cmd .= " $_" foreach (@files_to_scan);
684 if (system($xgettext_cmd) != 0) {
685 die "system call failed: $xgettext_cmd";
688 my $replace_charset_cmd = "$self->{sed} --in-place " .
689 "$Bin/$self->{domain}.pot " .
690 "--expression='s/charset=CHARSET/charset=UTF-8/'";
691 if (system($replace_charset_cmd) != 0) {
692 die "system call failed: $replace_charset_cmd";
696 sub install_messages
{
699 my $locale = $self->locale_name();
700 my $modir = "$self->{path_po}/$locale/LC_MESSAGES";
701 my $pofile = "$self->{path_po}/$self->{lang}-messages.po";
702 my $mofile = "$modir/$self->{domain}.mo";
704 if ( not -f
$pofile ) {
705 $self->create_messages();
707 say "Install messages ($locale)" if $self->{verbose
};
709 system "$self->{msgfmt} -o $mofile $pofile";
715 unlink "$Bin/$self->{domain}.pot";
719 my ($self, $files) = @_;
720 return unless $self->{lang
};
721 $self->install_tmpl($files) unless $self->{pref_only
};
722 $self->install_prefs();
723 $self->install_messages();
730 opendir( my $dh, $self->{path_po
} );
731 my @files = grep { $_ =~ /-pref.po$/ }
733 @files = map { $_ =~ s/-pref.po$//; $_ } @files;
738 my ($self, $files) = @_;
739 my @langs = $self->{lang
} ?
($self->{lang
}) : $self->get_all_langs();
740 for my $lang ( @langs ) {
741 $self->set_lang( $lang );
742 $self->update_tmpl($files) unless $self->{pref_only
};
743 $self->update_prefs();
744 $self->update_messages();
751 my ($self, $files) = @_;
752 return unless $self->{lang
};
753 $self->create_tmpl($files) unless $self->{pref_only
};
754 $self->create_prefs();
755 $self->create_messages();
766 LangInstaller.pm - Handle templates and preferences translation
770 my $installer = LangInstaller->new( 'fr-FR' );
771 $installer->create();
772 $installer->update();
773 $installer->install();
774 for my $lang ( @{$installer->{langs} ) {
775 $installer->set_lang( $lan );
776 $installer->install();
783 Create a new instance of the installer object.
787 For the current language, create .po files for templates and preferences based
788 of the english ('en') version.
792 For the current language, update .po files.
796 For the current langage C<$self->{lang}, use .po files to translate the english
797 version of templates and preferences files and copy those files in the
798 appropriate directory.
802 =item translate create F<lang>
804 Create 4 kinds of .po files in F<po> subdirectory:
805 (1) one from each theme on opac pages templates,
806 (2) intranet templates,
808 (4) one for each MARC dialect.
813 =item F<lang>-opac-{theme}.po
815 Contains extracted text from english (en) OPAC templates found in
816 <KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
818 =item F<lang>-staff-prog.po
820 Contains extracted text from english (en) intranet templates found in
821 <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
823 =item F<lang>-pref.po
825 Contains extracted text from english (en) preferences. They are found in files
826 located in <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/admin/preferences
829 =item F<lang>-marc-{MARC}.po
831 Contains extracted text from english (en) files from opac and intranet,
832 related with MARC dialects.
836 =item pref-trans update F<lang>
838 Update .po files in F<po> directory, named F<lang>-*.po.
840 =item pref-trans install F<lang>