Lines 22-57
use Modern::Perl;
Link Here
|
22 |
use C4::Context; |
22 |
use C4::Context; |
23 |
# WARNING: Any other tested YAML library fails to work properly in this |
23 |
# WARNING: Any other tested YAML library fails to work properly in this |
24 |
# script content |
24 |
# script content |
25 |
use YAML::Syck qw( Dump LoadFile DumpFile ); |
25 |
use YAML::Syck qw( LoadFile DumpFile ); |
26 |
use Locale::PO; |
26 |
use Locale::PO; |
27 |
use FindBin qw( $Bin ); |
27 |
use FindBin qw( $Bin ); |
28 |
use File::Basename; |
28 |
use File::Basename; |
29 |
use File::Find; |
|
|
30 |
use File::Path qw( make_path ); |
29 |
use File::Path qw( make_path ); |
31 |
use File::Copy; |
30 |
use File::Copy; |
32 |
use File::Slurp; |
|
|
33 |
use File::Spec; |
34 |
use File::Temp qw( tempdir tempfile ); |
35 |
use Template::Parser; |
36 |
use PPI; |
37 |
|
38 |
|
31 |
|
39 |
$YAML::Syck::ImplicitTyping = 1; |
32 |
$YAML::Syck::ImplicitTyping = 1; |
40 |
|
33 |
|
41 |
|
|
|
42 |
# Default file header for .po syspref files |
43 |
my $default_pref_po_header = Locale::PO->new(-msgid => '', -msgstr => |
44 |
"Project-Id-Version: PACKAGE VERSION\\n" . |
45 |
"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\n" . |
46 |
"Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n" . |
47 |
"Language-Team: Koha Translate List <koha-translate\@lists.koha-community.org>\\n" . |
48 |
"MIME-Version: 1.0\\n" . |
49 |
"Content-Type: text/plain; charset=UTF-8\\n" . |
50 |
"Content-Transfer-Encoding: 8bit\\n" . |
51 |
"Plural-Forms: nplurals=2; plural=(n > 1);\\n" |
52 |
); |
53 |
|
54 |
|
55 |
sub set_lang { |
34 |
sub set_lang { |
56 |
my ($self, $lang) = @_; |
35 |
my ($self, $lang) = @_; |
57 |
|
36 |
|
Lines 60-66
sub set_lang {
Link Here
|
60 |
"/prog/$lang/modules/admin/preferences"; |
39 |
"/prog/$lang/modules/admin/preferences"; |
61 |
} |
40 |
} |
62 |
|
41 |
|
63 |
|
|
|
64 |
sub new { |
42 |
sub new { |
65 |
my ($class, $lang, $pref_only, $verbose) = @_; |
43 |
my ($class, $lang, $pref_only, $verbose) = @_; |
66 |
|
44 |
|
Lines 75-106
sub new {
Link Here
|
75 |
$self->{verbose} = $verbose; |
53 |
$self->{verbose} = $verbose; |
76 |
$self->{process} = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q'); |
54 |
$self->{process} = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q'); |
77 |
$self->{path_po} = "$Bin/po"; |
55 |
$self->{path_po} = "$Bin/po"; |
78 |
$self->{po} = { '' => $default_pref_po_header }; |
56 |
$self->{po} = {}; |
79 |
$self->{domain} = 'Koha'; |
57 |
$self->{domain} = 'Koha'; |
80 |
$self->{cp} = `which cp`; |
|
|
81 |
$self->{msgmerge} = `which msgmerge`; |
82 |
$self->{msgfmt} = `which msgfmt`; |
58 |
$self->{msgfmt} = `which msgfmt`; |
83 |
$self->{msginit} = `which msginit`; |
|
|
84 |
$self->{msgattrib} = `which msgattrib`; |
85 |
$self->{xgettext} = `which xgettext`; |
86 |
$self->{sed} = `which sed`; |
87 |
$self->{po2json} = "$Bin/po2json"; |
59 |
$self->{po2json} = "$Bin/po2json"; |
88 |
$self->{gzip} = `which gzip`; |
60 |
$self->{gzip} = `which gzip`; |
89 |
$self->{gunzip} = `which gunzip`; |
61 |
$self->{gunzip} = `which gunzip`; |
90 |
chomp $self->{cp}; |
|
|
91 |
chomp $self->{msgmerge}; |
92 |
chomp $self->{msgfmt}; |
62 |
chomp $self->{msgfmt}; |
93 |
chomp $self->{msginit}; |
|
|
94 |
chomp $self->{msgattrib}; |
95 |
chomp $self->{xgettext}; |
96 |
chomp $self->{sed}; |
97 |
chomp $self->{gzip}; |
63 |
chomp $self->{gzip}; |
98 |
chomp $self->{gunzip}; |
64 |
chomp $self->{gunzip}; |
99 |
|
65 |
|
100 |
unless ($self->{xgettext}) { |
|
|
101 |
die "Missing 'xgettext' executable. Have you installed the gettext package?\n"; |
102 |
} |
103 |
|
104 |
# Get all .pref file names |
66 |
# Get all .pref file names |
105 |
opendir my $fh, $self->{path_pref_en}; |
67 |
opendir my $fh, $self->{path_pref_en}; |
106 |
my @pref_files = grep { /\.pref$/ } readdir($fh); |
68 |
my @pref_files = grep { /\.pref$/ } readdir($fh); |
Lines 175-181
sub new {
Link Here
|
175 |
bless $self, $class; |
137 |
bless $self, $class; |
176 |
} |
138 |
} |
177 |
|
139 |
|
178 |
|
|
|
179 |
sub po_filename { |
140 |
sub po_filename { |
180 |
my $self = shift; |
141 |
my $self = shift; |
181 |
my $suffix = shift; |
142 |
my $suffix = shift; |
Lines 186-347
sub po_filename {
Link Here
|
186 |
return $trans_file; |
147 |
return $trans_file; |
187 |
} |
148 |
} |
188 |
|
149 |
|
|
|
150 |
sub get_trans_text { |
151 |
my ($self, $msgid, $default) = @_; |
189 |
|
152 |
|
190 |
sub po_append { |
153 |
my $po = $self->{po}->{Locale::PO->quote($msgid)}; |
191 |
my ($self, $id, $comment) = @_; |
154 |
if ($po) { |
192 |
my $po = $self->{po}; |
155 |
my $msgstr = Locale::PO->dequote($po->msgstr); |
193 |
my $p = $po->{$id}; |
156 |
if ($msgstr and length($msgstr) > 0) { |
194 |
if ( $p ) { |
157 |
return $msgstr; |
195 |
$p->comment( $p->comment . "\n" . $comment ); |
|
|
196 |
} |
197 |
else { |
198 |
$po->{$id} = Locale::PO->new( |
199 |
-comment => $comment, |
200 |
-msgid => $id, |
201 |
-msgstr => '' |
202 |
); |
203 |
} |
204 |
} |
205 |
|
206 |
|
207 |
sub add_prefs { |
208 |
my ($self, $comment, $prefs) = @_; |
209 |
|
210 |
for my $pref ( @$prefs ) { |
211 |
my $pref_name = ''; |
212 |
for my $element ( @$pref ) { |
213 |
if ( ref( $element) eq 'HASH' ) { |
214 |
$pref_name = $element->{pref}; |
215 |
last; |
216 |
} |
217 |
} |
218 |
for my $element ( @$pref ) { |
219 |
if ( ref( $element) eq 'HASH' ) { |
220 |
while ( my ($key, $value) = each(%$element) ) { |
221 |
next unless $key eq 'choices' or $key eq 'multiple'; |
222 |
next unless ref($value) eq 'HASH'; |
223 |
for my $ckey ( keys %$value ) { |
224 |
my $id = $self->{file} . "#$pref_name# " . $value->{$ckey}; |
225 |
$self->po_append( $id, $comment ); |
226 |
} |
227 |
} |
228 |
} |
229 |
elsif ( $element ) { |
230 |
$self->po_append( $self->{file} . "#$pref_name# $element", $comment ); |
231 |
} |
232 |
} |
158 |
} |
233 |
} |
159 |
} |
234 |
} |
|
|
235 |
|
236 |
|
237 |
sub get_trans_text { |
238 |
my ($self, $id) = @_; |
239 |
|
160 |
|
240 |
my $po = $self->{po}->{$id}; |
161 |
return $default; |
241 |
return unless $po; |
|
|
242 |
return Locale::PO->dequote($po->msgstr); |
243 |
} |
162 |
} |
244 |
|
163 |
|
|
|
164 |
sub get_translated_tab_content { |
165 |
my ($self, $file, $tab_content) = @_; |
245 |
|
166 |
|
246 |
sub update_tab_prefs { |
167 |
if ( ref($tab_content) eq 'ARRAY' ) { |
247 |
my ($self, $pref, $prefs) = @_; |
168 |
return $self->get_translated_prefs($file, $tab_content); |
248 |
|
|
|
249 |
for my $p ( @$prefs ) { |
250 |
my $pref_name = ''; |
251 |
next unless $p; |
252 |
for my $element ( @$p ) { |
253 |
if ( ref( $element) eq 'HASH' ) { |
254 |
$pref_name = $element->{pref}; |
255 |
last; |
256 |
} |
257 |
} |
258 |
for my $i ( 0..@$p-1 ) { |
259 |
my $element = $p->[$i]; |
260 |
if ( ref( $element) eq 'HASH' ) { |
261 |
while ( my ($key, $value) = each(%$element) ) { |
262 |
next unless $key eq 'choices' or $key eq 'multiple'; |
263 |
next unless ref($value) eq 'HASH'; |
264 |
for my $ckey ( keys %$value ) { |
265 |
my $id = $self->{file} . "#$pref_name# " . $value->{$ckey}; |
266 |
my $text = $self->get_trans_text( $id ); |
267 |
$value->{$ckey} = $text if $text; |
268 |
} |
269 |
} |
270 |
} |
271 |
elsif ( $element ) { |
272 |
my $id = $self->{file} . "#$pref_name# $element"; |
273 |
my $text = $self->get_trans_text( $id ); |
274 |
$p->[$i] = $text if $text; |
275 |
} |
276 |
} |
277 |
} |
169 |
} |
278 |
} |
|
|
279 |
|
170 |
|
|
|
171 |
my $translated_tab_content = { |
172 |
map { |
173 |
my $section = $_; |
174 |
my $sysprefs = $tab_content->{$section}; |
175 |
my $msgid = sprintf('%s %s', $file, $section); |
280 |
|
176 |
|
281 |
sub get_po_from_prefs { |
177 |
$self->get_trans_text($msgid, $section) => $self->get_translated_prefs($file, $sysprefs); |
282 |
my $self = shift; |
178 |
} keys %$tab_content |
|
|
179 |
}; |
283 |
|
180 |
|
284 |
for my $file ( @{$self->{pref_files}} ) { |
181 |
return $translated_tab_content; |
285 |
my $pref = LoadFile( $self->{path_pref_en} . "/$file" ); |
|
|
286 |
$self->{file} = $file; |
287 |
# Entries for tab titles |
288 |
$self->po_append( $self->{file}, $_ ) for keys %$pref; |
289 |
while ( my ($tab, $tab_content) = each %$pref ) { |
290 |
if ( ref($tab_content) eq 'ARRAY' ) { |
291 |
$self->add_prefs( $tab, $tab_content ); |
292 |
next; |
293 |
} |
294 |
while ( my ($section, $sysprefs) = each %$tab_content ) { |
295 |
my $comment = "$tab > $section"; |
296 |
$self->po_append( $self->{file} . " " . $section, $comment ); |
297 |
$self->add_prefs( $comment, $sysprefs ); |
298 |
} |
299 |
} |
300 |
} |
301 |
} |
182 |
} |
302 |
|
183 |
|
|
|
184 |
sub get_translated_prefs { |
185 |
my ($self, $file, $sysprefs) = @_; |
303 |
|
186 |
|
304 |
sub save_po { |
187 |
my $translated_prefs = [ |
305 |
my $self = shift; |
188 |
map { |
|
|
189 |
my ($pref_elt) = grep { ref($_) eq 'HASH' && exists $_->{pref} } @$_; |
190 |
my $pref_name = $pref_elt ? $pref_elt->{pref} : ''; |
191 |
|
192 |
my $translated_syspref = [ |
193 |
map { |
194 |
$self->get_translated_pref($file, $pref_name, $_); |
195 |
} @$_ |
196 |
]; |
306 |
|
197 |
|
307 |
# Create file header if it doesn't already exist |
198 |
$translated_syspref; |
308 |
my $po = $self->{po}; |
199 |
} @$sysprefs |
309 |
$po->{''} ||= $default_pref_po_header; |
200 |
]; |
310 |
|
201 |
|
311 |
# Write .po entries into a file put in Koha standard po directory |
202 |
return $translated_prefs; |
312 |
Locale::PO->save_file_fromhash( $self->po_filename("-pref.po"), $po ); |
|
|
313 |
say "Saved in file: ", $self->po_filename("-pref.po") if $self->{verbose}; |
314 |
} |
203 |
} |
315 |
|
204 |
|
|
|
205 |
sub get_translated_pref { |
206 |
my ($self, $file, $pref_name, $syspref) = @_; |
316 |
|
207 |
|
317 |
sub get_po_merged_with_en { |
208 |
unless (ref($syspref)) { |
318 |
my $self = shift; |
209 |
$syspref //= ''; |
319 |
|
210 |
my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $syspref); |
320 |
# Get po from current 'en' .pref files |
211 |
return $self->get_trans_text($msgid, $syspref); |
321 |
$self->get_po_from_prefs(); |
212 |
} |
322 |
my $po_current = $self->{po}; |
|
|
323 |
|
213 |
|
324 |
# Get po from previous generation |
214 |
my $translated_pref = { |
325 |
my $po_previous = Locale::PO->load_file_ashash( $self->po_filename("-pref.po") ); |
215 |
map { |
|
|
216 |
my $key = $_; |
217 |
my $value = $syspref->{$key}; |
326 |
|
218 |
|
327 |
for my $id ( keys %$po_current ) { |
219 |
my $translated_value = $value; |
328 |
my $po = $po_previous->{Locale::PO->quote($id)}; |
220 |
if (($key eq 'choices' || $key eq 'multiple') && ref($value) eq 'HASH') { |
329 |
next unless $po; |
221 |
$translated_value = { |
330 |
my $text = Locale::PO->dequote( $po->msgstr ); |
222 |
map { |
331 |
$po_current->{$id}->msgstr( $text ); |
223 |
my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $value->{$_}); |
332 |
} |
224 |
$_ => $self->get_trans_text($msgid, $value->{$_}) |
333 |
} |
225 |
} keys %$value |
|
|
226 |
} |
227 |
} |
334 |
|
228 |
|
|
|
229 |
$key => $translated_value |
230 |
} keys %$syspref |
231 |
}; |
335 |
|
232 |
|
336 |
sub update_prefs { |
233 |
return $translated_pref; |
337 |
my $self = shift; |
|
|
338 |
print "Update '", $self->{lang}, |
339 |
"' preferences .po file from 'en' .pref files\n" if $self->{verbose}; |
340 |
$self->get_po_merged_with_en(); |
341 |
$self->save_po(); |
342 |
} |
234 |
} |
343 |
|
235 |
|
344 |
|
|
|
345 |
sub install_prefs { |
236 |
sub install_prefs { |
346 |
my $self = shift; |
237 |
my $self = shift; |
347 |
|
238 |
|
Lines 350-394
sub install_prefs {
Link Here
|
350 |
exit; |
241 |
exit; |
351 |
} |
242 |
} |
352 |
|
243 |
|
353 |
# Get the language .po file merged with last modified 'en' preferences |
244 |
$self->{po} = Locale::PO->load_file_ashash($self->po_filename("-pref.po"), 'utf8'); |
354 |
$self->get_po_merged_with_en(); |
|
|
355 |
|
245 |
|
356 |
for my $file ( @{$self->{pref_files}} ) { |
246 |
for my $file ( @{$self->{pref_files}} ) { |
357 |
my $pref = LoadFile( $self->{path_pref_en} . "/$file" ); |
247 |
my $pref = LoadFile( $self->{path_pref_en} . "/$file" ); |
358 |
$self->{file} = $file; |
248 |
|
359 |
# First, keys are replaced (tab titles) |
249 |
my $translated_pref = { |
360 |
$pref = do { |
250 |
map { |
361 |
my %pref = map { |
251 |
my $tab = $_; |
362 |
$self->get_trans_text( $self->{file} ) || $_ => $pref->{$_} |
252 |
my $tab_content = $pref->{$tab}; |
363 |
} keys %$pref; |
253 |
|
364 |
\%pref; |
254 |
$self->get_trans_text($file, $tab) => $self->get_translated_tab_content($file, $tab_content); |
|
|
255 |
} keys %$pref |
365 |
}; |
256 |
}; |
366 |
while ( my ($tab, $tab_content) = each %$pref ) { |
257 |
|
367 |
if ( ref($tab_content) eq 'ARRAY' ) { |
258 |
|
368 |
$self->update_tab_prefs( $pref, $tab_content ); |
|
|
369 |
next; |
370 |
} |
371 |
while ( my ($section, $sysprefs) = each %$tab_content ) { |
372 |
$self->update_tab_prefs( $pref, $sysprefs ); |
373 |
} |
374 |
my $ntab = {}; |
375 |
for my $section ( keys %$tab_content ) { |
376 |
my $id = $self->{file} . " $section"; |
377 |
my $text = $self->get_trans_text($id); |
378 |
my $nsection = $text ? $text : $section; |
379 |
if( exists $ntab->{$nsection} ) { |
380 |
# When translations collide (see BZ 18634) |
381 |
push @{$ntab->{$nsection}}, @{$tab_content->{$section}}; |
382 |
} else { |
383 |
$ntab->{$nsection} = $tab_content->{$section}; |
384 |
} |
385 |
} |
386 |
$pref->{$tab} = $ntab; |
387 |
} |
388 |
my $file_trans = $self->{po_path_lang} . "/$file"; |
259 |
my $file_trans = $self->{po_path_lang} . "/$file"; |
389 |
print "Write $file\n" if $self->{verbose}; |
260 |
print "Write $file\n" if $self->{verbose}; |
390 |
open my $fh, ">", $file_trans; |
261 |
DumpFile($file_trans, $translated_pref); |
391 |
print $fh Dump($pref); |
|
|
392 |
} |
262 |
} |
393 |
} |
263 |
} |
394 |
|
264 |
|
Lines 429-608
sub install_tmpl {
Link Here
|
429 |
} |
299 |
} |
430 |
} |
300 |
} |
431 |
|
301 |
|
432 |
|
|
|
433 |
sub update_tmpl { |
434 |
my ($self, $files) = @_; |
435 |
|
436 |
say "Update templates" if $self->{verbose}; |
437 |
for my $trans ( @{$self->{interface}} ) { |
438 |
my @files = @$files; |
439 |
my @nomarc = (); |
440 |
print |
441 |
" Update templates '$trans->{name}'\n", |
442 |
" From: $trans->{dir}/en/\n", |
443 |
" To : $self->{path_po}/$self->{lang}$trans->{suffix}\n" |
444 |
if $self->{verbose}; |
445 |
|
446 |
my $trans_dir = join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs |
447 |
# if processing MARC po file, only use corresponding files |
448 |
my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations |
449 |
# if not processing MARC po file, ignore all MARC files |
450 |
@nomarc = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants |
451 |
|
452 |
system |
453 |
"$self->{process} update " . |
454 |
"-i $trans_dir " . |
455 |
"-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " . |
456 |
"$marc " . |
457 |
( @files ? ' -f ' . join ' -f ', @files : '') . |
458 |
( @nomarc ? ' -n ' . join ' -n ', @nomarc : ''); |
459 |
} |
460 |
} |
461 |
|
462 |
|
463 |
sub create_prefs { |
464 |
my $self = shift; |
465 |
|
466 |
if ( -e $self->po_filename("-pref.po") ) { |
467 |
say "Preferences .po file already exists. Delete it if you want to recreate it."; |
468 |
return; |
469 |
} |
470 |
$self->get_po_from_prefs(); |
471 |
$self->save_po(); |
472 |
} |
473 |
|
474 |
sub get_po_from_target { |
475 |
my $self = shift; |
476 |
my $target = shift; |
477 |
|
478 |
my $po; |
479 |
my $po_head = new Locale::PO; |
480 |
$po_head->{msgid} = "\"\""; |
481 |
$po_head->{msgstr} = "". |
482 |
"Project-Id-Version: Koha Project - Installation files\\n" . |
483 |
"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\n" . |
484 |
"Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n" . |
485 |
"Language-Team: Koha Translation Team\\n" . |
486 |
"Language: ".$self->{lang}."\\n" . |
487 |
"MIME-Version: 1.0\\n" . |
488 |
"Content-Type: text/plain; charset=UTF-8\\n" . |
489 |
"Content-Transfer-Encoding: 8bit\\n"; |
490 |
|
491 |
my @dirs = @{ $target->{dirs} }; |
492 |
my $intradir = $self->{context}->config('intranetdir'); |
493 |
for my $dir ( @dirs ) { # each dir |
494 |
opendir( my $dh, "$intradir/$dir" ) or die ("Can't open $intradir/$dir"); |
495 |
my @filelist = grep { $_ =~ m/\.yml/ } readdir($dh); # Just yaml files |
496 |
close($dh); |
497 |
for my $file ( @filelist ) { # each file |
498 |
my $yaml = LoadFile( "$intradir/$dir/$file" ); |
499 |
my @tables = @{ $yaml->{'tables'} }; |
500 |
my $tablec; |
501 |
for my $table ( @tables ) { # each table |
502 |
$tablec++; |
503 |
my $table_name = ( keys %$table )[0]; |
504 |
my @translatable = @{ $table->{$table_name}->{translatable} }; |
505 |
my @rows = @{ $table->{$table_name}->{rows} }; |
506 |
my @multiline = @{ $table->{$table_name}->{'multiline'} }; # to check multiline values |
507 |
my $rowc; |
508 |
for my $row ( @rows ) { # each row |
509 |
$rowc++; |
510 |
for my $field ( @translatable ) { # each field |
511 |
if ( @multiline and grep { $_ eq $field } @multiline ) { # multiline fields, only notices ATM |
512 |
my $mulc; |
513 |
foreach my $line ( @{$row->{$field}} ) { |
514 |
$mulc++; |
515 |
next if ( $line =~ /^(\s*<.*?>\s*$|^\s*\[.*?\]\s*|\s*)$/ ); # discard pure html, TT, empty |
516 |
$line =~ s/(<<.*?>>|\[\%.*?\%\]|<.*?>)/\%s/g; # put placeholders |
517 |
next if ( $line =~ /^(\s|%s|-|[[:punct:]]|\(|\))*$/ or length($line) < 2 ); # discard non strings |
518 |
if ( not $po->{ $line } ) { |
519 |
my $msg = new Locale::PO( |
520 |
-msgid => $line, -msgstr => '', |
521 |
-reference => "$dir/$file:$table_name:$tablec:row:$rowc:mul:$mulc" ); |
522 |
$po->{ $line } = $msg; |
523 |
} |
524 |
} |
525 |
} else { |
526 |
if ( defined $row->{$field} and length($row->{$field}) > 1 # discard null values and small strings |
527 |
and not $po->{ $row->{$field} } ) { |
528 |
my $msg = new Locale::PO( |
529 |
-msgid => $row->{$field}, -msgstr => '', |
530 |
-reference => "$dir/$file:$table_name:$tablec:row:$rowc" ); |
531 |
$po->{ $row->{$field} } = $msg; |
532 |
} |
533 |
} |
534 |
} |
535 |
} |
536 |
} |
537 |
my $desccount; |
538 |
for my $description ( @{ $yaml->{'description'} } ) { |
539 |
$desccount++; |
540 |
if ( length($description) > 1 and not $po->{ $description } ) { |
541 |
my $msg = new Locale::PO( |
542 |
-msgid => $description, -msgstr => '', |
543 |
-reference => "$dir/$file:description:$desccount" ); |
544 |
$po->{ $description } = $msg; |
545 |
} |
546 |
} |
547 |
} |
548 |
} |
549 |
$po->{''} = $po_head if ( $po ); |
550 |
|
551 |
return $po; |
552 |
} |
553 |
|
554 |
sub create_installer { |
555 |
my $self = shift; |
556 |
return unless ( $self->{installer} ); |
557 |
|
558 |
say "Create installer translation files\n" if $self->{verbose}; |
559 |
|
560 |
my @targets = @{ $self->{installer} }; # each installer target (common,marc21,unimarc) |
561 |
|
562 |
for my $target ( @targets ) { |
563 |
if ( -e $self->po_filename( $target->{suffix} ) ) { |
564 |
say "$self->{lang}$target->{suffix} file already exists. Delete it if you want to recreate it."; |
565 |
return; |
566 |
} |
567 |
} |
568 |
|
569 |
for my $target ( @targets ) { |
570 |
my $po = get_po_from_target( $self, $target ); |
571 |
# create output file only if there is something to write |
572 |
if ( $po ) { |
573 |
my $po_file = $self->po_filename( $target->{suffix} ); |
574 |
Locale::PO->save_file_fromhash( $po_file, $po ); |
575 |
say "Saved in file: ", $po_file if $self->{verbose}; |
576 |
} |
577 |
} |
578 |
} |
579 |
|
580 |
sub update_installer { |
581 |
my $self = shift; |
582 |
return unless ( $self->{installer} ); |
583 |
|
584 |
say "Update installer translation files\n" if $self->{verbose}; |
585 |
|
586 |
my @targets = @{ $self->{installer} }; # each installer target (common,marc21,unimarc) |
587 |
|
588 |
for my $target ( @targets ) { |
589 |
return unless ( -e $self->po_filename( $target->{suffix} ) ); |
590 |
my $po = get_po_from_target( $self, $target ); |
591 |
# update file only if there is something to update |
592 |
if ( $po ) { |
593 |
my ( $fh, $po_temp ) = tempfile(); |
594 |
binmode( $fh, ":encoding(UTF-8)" ); |
595 |
Locale::PO->save_file_fromhash( $po_temp, $po ); |
596 |
my $po_file = $self->po_filename( $target->{suffix} ); |
597 |
eval { |
598 |
my $st = system($self->{msgmerge}." ".($self->{verbose}?'':'-q'). |
599 |
" -s $po_file $po_temp -o - | ".$self->{msgattrib}." --no-obsolete -o $po_file"); |
600 |
}; |
601 |
say "Updated file: ", $po_file if $self->{verbose}; |
602 |
} |
603 |
} |
604 |
} |
605 |
|
606 |
sub translate_yaml { |
302 |
sub translate_yaml { |
607 |
my $self = shift; |
303 |
my $self = shift; |
608 |
my $target = shift; |
304 |
my $target = shift; |
Lines 716-750
sub install_installer {
Link Here
|
716 |
} |
412 |
} |
717 |
} |
413 |
} |
718 |
|
414 |
|
719 |
sub create_tmpl { |
|
|
720 |
my ($self, $files) = @_; |
721 |
|
722 |
say "Create templates\n" if $self->{verbose}; |
723 |
for my $trans ( @{$self->{interface}} ) { |
724 |
my @files = @$files; |
725 |
my @nomarc = (); |
726 |
print |
727 |
" Create templates .po files for '$trans->{name}'\n", |
728 |
" From: $trans->{dir}/en/\n", |
729 |
" To : $self->{path_po}/$self->{lang}$trans->{suffix}\n" |
730 |
if $self->{verbose}; |
731 |
|
732 |
my $trans_dir = join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs |
733 |
# if processing MARC po file, only use corresponding files |
734 |
my $marc = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":""; # for MARC translations |
735 |
# if not processing MARC po file, ignore all MARC files |
736 |
@nomarc = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants |
737 |
|
738 |
system |
739 |
"$self->{process} create " . |
740 |
"-i $trans_dir " . |
741 |
"-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " . |
742 |
"$marc " . |
743 |
( @files ? ' -f ' . join ' -f ', @files : '') . |
744 |
( @nomarc ? ' -n ' . join ' -n ', @nomarc : ''); |
745 |
} |
746 |
} |
747 |
|
748 |
sub locale_name { |
415 |
sub locale_name { |
749 |
my $self = shift; |
416 |
my $self = shift; |
750 |
|
417 |
|
Lines 758-1007
sub locale_name {
Link Here
|
758 |
return $locale; |
425 |
return $locale; |
759 |
} |
426 |
} |
760 |
|
427 |
|
761 |
sub create_messages { |
|
|
762 |
my $self = shift; |
763 |
|
764 |
my $pot = "$Bin/$self->{domain}.pot"; |
765 |
my $po = "$self->{path_po}/$self->{lang}-messages.po"; |
766 |
my $js_pot = "$self->{domain}-js.pot"; |
767 |
my $js_po = "$self->{path_po}/$self->{lang}-messages-js.po"; |
768 |
|
769 |
unless ( -f $pot && -f $js_pot ) { |
770 |
$self->extract_messages(); |
771 |
} |
772 |
|
773 |
say "Create messages ($self->{lang})" if $self->{verbose}; |
774 |
my $locale = $self->locale_name(); |
775 |
system "$self->{msginit} -i $pot -o $po -l $locale --no-translator 2> /dev/null"; |
776 |
warn "Problems creating $pot ".$? if ( $? == -1 ); |
777 |
system "$self->{msginit} -i $js_pot -o $js_po -l $locale --no-translator 2> /dev/null"; |
778 |
warn "Problems creating $js_pot ".$? if ( $? == -1 ); |
779 |
|
780 |
# If msginit failed to correctly set Plural-Forms, set a default one |
781 |
system "$self->{sed} --in-place " |
782 |
. "--expression='s/Plural-Forms: nplurals=INTEGER; plural=EXPRESSION/Plural-Forms: nplurals=2; plural=(n != 1)/' " |
783 |
. "$po $js_po"; |
784 |
} |
785 |
|
786 |
sub update_messages { |
787 |
my $self = shift; |
788 |
|
789 |
my $pot = "$Bin/$self->{domain}.pot"; |
790 |
my $po = "$self->{path_po}/$self->{lang}-messages.po"; |
791 |
my $js_pot = "$self->{domain}-js.pot"; |
792 |
my $js_po = "$self->{path_po}/$self->{lang}-messages-js.po"; |
793 |
|
794 |
unless ( -f $pot && -f $js_pot ) { |
795 |
$self->extract_messages(); |
796 |
} |
797 |
|
798 |
if ( -f $po && -f $js_pot ) { |
799 |
say "Update messages ($self->{lang})" if $self->{verbose}; |
800 |
system "$self->{msgmerge} --backup=off --quiet -U $po $pot"; |
801 |
system "$self->{msgmerge} --backup=off --quiet -U $js_po $js_pot"; |
802 |
} else { |
803 |
$self->create_messages(); |
804 |
} |
805 |
} |
806 |
|
807 |
sub extract_messages_from_templates { |
808 |
my ($self, $tempdir, $type, @files) = @_; |
809 |
|
810 |
my $htdocs = $type eq 'intranet' ? 'intrahtdocs' : 'opachtdocs'; |
811 |
my $dir = $self->{context}->config($htdocs); |
812 |
my @keywords = qw(t tx tn txn tnx tp tpx tnp tnpx); |
813 |
my $parser = Template::Parser->new(); |
814 |
|
815 |
foreach my $file (@files) { |
816 |
say "Extract messages from $file" if $self->{verbose}; |
817 |
my $template = read_file(File::Spec->catfile($dir, $file)); |
818 |
|
819 |
# No need to process a file that doesn't use the i18n.inc file. |
820 |
next unless $template =~ /i18n\.inc/; |
821 |
|
822 |
my $data = $parser->parse($template); |
823 |
unless ($data) { |
824 |
warn "Error at $file : " . $parser->error(); |
825 |
next; |
826 |
} |
827 |
|
828 |
my $destfile = $type eq 'intranet' ? |
829 |
File::Spec->catfile($tempdir, 'koha-tmpl', 'intranet-tmpl', $file) : |
830 |
File::Spec->catfile($tempdir, 'koha-tmpl', 'opac-tmpl', $file); |
831 |
|
832 |
make_path(dirname($destfile)); |
833 |
open my $fh, '>', $destfile; |
834 |
|
835 |
my @blocks = ($data->{BLOCK}, values %{ $data->{DEFBLOCKS} }); |
836 |
foreach my $block (@blocks) { |
837 |
my $document = PPI::Document->new(\$block); |
838 |
|
839 |
# [% t('foo') %] is compiled to |
840 |
# $output .= $stash->get(['t', ['foo']]); |
841 |
# We try to find all nodes corresponding to keyword (here 't') |
842 |
my $nodes = $document->find(sub { |
843 |
my ($topnode, $element) = @_; |
844 |
|
845 |
# Filter out non-valid keywords |
846 |
return 0 unless ($element->isa('PPI::Token::Quote::Single')); |
847 |
return 0 unless (grep {$element->content eq qq{'$_'}} @keywords); |
848 |
|
849 |
# keyword (e.g. 't') should be the first element of the arrayref |
850 |
# passed to $stash->get() |
851 |
return 0 if $element->sprevious_sibling; |
852 |
|
853 |
return 0 unless $element->snext_sibling |
854 |
&& $element->snext_sibling->snext_sibling |
855 |
&& $element->snext_sibling->snext_sibling->isa('PPI::Structure::Constructor'); |
856 |
|
857 |
# Check that it's indeed a call to $stash->get() |
858 |
my $statement = $element->statement->parent->statement->parent->statement; |
859 |
return 0 unless grep { $_->isa('PPI::Token::Symbol') && $_->content eq '$stash' } $statement->children; |
860 |
return 0 unless grep { $_->isa('PPI::Token::Operator') && $_->content eq '->' } $statement->children; |
861 |
return 0 unless grep { $_->isa('PPI::Token::Word') && $_->content eq 'get' } $statement->children; |
862 |
|
863 |
return 1; |
864 |
}); |
865 |
|
866 |
next unless $nodes; |
867 |
|
868 |
# Write the Perl equivalent of calls to t* functions family, so |
869 |
# xgettext can extract the strings correctly |
870 |
foreach my $node (@$nodes) { |
871 |
my @args = map { |
872 |
$_->significant && !$_->isa('PPI::Token::Operator') ? $_->content : () |
873 |
} $node->snext_sibling->snext_sibling->find_first('PPI::Statement')->children; |
874 |
|
875 |
my $keyword = $node->content; |
876 |
$keyword =~ s/^'t(.*)'$/__$1/; |
877 |
|
878 |
# Only keep required args to have a clean output |
879 |
my @required_args = shift @args; |
880 |
push @required_args, shift @args if $keyword =~ /n/; |
881 |
push @required_args, shift @args if $keyword =~ /p/; |
882 |
|
883 |
say $fh "$keyword(" . join(', ', @required_args) . ");"; |
884 |
} |
885 |
|
886 |
} |
887 |
|
888 |
close $fh; |
889 |
} |
890 |
|
891 |
return $tempdir; |
892 |
} |
893 |
|
894 |
sub extract_messages { |
895 |
my $self = shift; |
896 |
|
897 |
say "Extract messages into POT file" if $self->{verbose}; |
898 |
|
899 |
my $intranetdir = $self->{context}->config('intranetdir'); |
900 |
my $opacdir = $self->{context}->config('opacdir'); |
901 |
|
902 |
# Find common ancestor directory |
903 |
my @intranetdirs = File::Spec->splitdir($intranetdir); |
904 |
my @opacdirs = File::Spec->splitdir($opacdir); |
905 |
my @basedirs; |
906 |
while (@intranetdirs and @opacdirs) { |
907 |
my ($dir1, $dir2) = (shift @intranetdirs, shift @opacdirs); |
908 |
last if $dir1 ne $dir2; |
909 |
push @basedirs, $dir1; |
910 |
} |
911 |
my $basedir = File::Spec->catdir(@basedirs); |
912 |
|
913 |
my @files_to_scan; |
914 |
my @directories_to_scan = ('.'); |
915 |
my @blacklist = map { File::Spec->catdir(@intranetdirs, $_) } qw(blib koha-tmpl skel tmp t); |
916 |
while (@directories_to_scan) { |
917 |
my $dir = shift @directories_to_scan; |
918 |
opendir DIR, File::Spec->catdir($basedir, $dir) or die "Unable to open $dir: $!"; |
919 |
foreach my $entry (readdir DIR) { |
920 |
next if $entry =~ /^\./; |
921 |
my $relentry = File::Spec->catfile($dir, $entry); |
922 |
my $abspath = File::Spec->catfile($basedir, $relentry); |
923 |
if (-d $abspath and not grep { $_ eq $relentry } @blacklist) { |
924 |
push @directories_to_scan, $relentry; |
925 |
} elsif (-f $abspath and $relentry =~ /\.(pl|pm)$/) { |
926 |
push @files_to_scan, $relentry; |
927 |
} |
928 |
} |
929 |
} |
930 |
|
931 |
my $intrahtdocs = $self->{context}->config('intrahtdocs'); |
932 |
my $opachtdocs = $self->{context}->config('opachtdocs'); |
933 |
|
934 |
my @intranet_tt_files; |
935 |
find(sub { |
936 |
if ($File::Find::dir =~ m|/en/| && $_ =~ m/\.(tt|inc)$/) { |
937 |
my $filename = $File::Find::name; |
938 |
$filename =~ s|^$intrahtdocs/||; |
939 |
push @intranet_tt_files, $filename; |
940 |
} |
941 |
}, $intrahtdocs); |
942 |
|
943 |
my @opac_tt_files; |
944 |
find(sub { |
945 |
if ($File::Find::dir =~ m|/en/| && $_ =~ m/\.(tt|inc)$/) { |
946 |
my $filename = $File::Find::name; |
947 |
$filename =~ s|^$opachtdocs/||; |
948 |
push @opac_tt_files, $filename; |
949 |
} |
950 |
}, $opachtdocs); |
951 |
|
952 |
my $tempdir = tempdir('Koha-translate-XXXX', TMPDIR => 1, CLEANUP => 1); |
953 |
$self->extract_messages_from_templates($tempdir, 'intranet', @intranet_tt_files); |
954 |
$self->extract_messages_from_templates($tempdir, 'opac', @opac_tt_files); |
955 |
|
956 |
@intranet_tt_files = map { File::Spec->catfile('koha-tmpl', 'intranet-tmpl', $_) } @intranet_tt_files; |
957 |
@opac_tt_files = map { File::Spec->catfile('koha-tmpl', 'opac-tmpl', $_) } @opac_tt_files; |
958 |
my @tt_files = grep { -e File::Spec->catfile($tempdir, $_) } @intranet_tt_files, @opac_tt_files; |
959 |
|
960 |
push @files_to_scan, @tt_files; |
961 |
|
962 |
my $xgettext_common_args = "--force-po --from-code=UTF-8 " |
963 |
. "--package-name=Koha --package-version='' " |
964 |
. "-k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 -k__p:1c,2 " |
965 |
. "-k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ -kN__n:1,2 " |
966 |
. "-kN__p:1c,2 -kN__np:1c,2,3 "; |
967 |
my $xgettext_cmd = "$self->{xgettext} -L Perl $xgettext_common_args " |
968 |
. "-o $Bin/$self->{domain}.pot -D $tempdir -D $basedir"; |
969 |
$xgettext_cmd .= " $_" foreach (@files_to_scan); |
970 |
|
971 |
if (system($xgettext_cmd) != 0) { |
972 |
die "system call failed: $xgettext_cmd"; |
973 |
} |
974 |
|
975 |
my @js_dirs = ( |
976 |
"$intrahtdocs/prog/js", |
977 |
"$opachtdocs/bootstrap/js", |
978 |
); |
979 |
|
980 |
my @js_files; |
981 |
find(sub { |
982 |
if ($_ =~ m/\.js$/) { |
983 |
my $filename = $File::Find::name; |
984 |
$filename =~ s|^$intranetdir/||; |
985 |
push @js_files, $filename; |
986 |
} |
987 |
}, @js_dirs); |
988 |
|
989 |
$xgettext_cmd = "$self->{xgettext} -L JavaScript $xgettext_common_args " |
990 |
. "-o $Bin/$self->{domain}-js.pot -D $intranetdir"; |
991 |
$xgettext_cmd .= " $_" foreach (@js_files); |
992 |
|
993 |
if (system($xgettext_cmd) != 0) { |
994 |
die "system call failed: $xgettext_cmd"; |
995 |
} |
996 |
|
997 |
my $replace_charset_cmd = "$self->{sed} --in-place " . |
998 |
"--expression='s/charset=CHARSET/charset=UTF-8/' " . |
999 |
"$Bin/$self->{domain}.pot $Bin/$self->{domain}-js.pot"; |
1000 |
if (system($replace_charset_cmd) != 0) { |
1001 |
die "system call failed: $replace_charset_cmd"; |
1002 |
} |
1003 |
} |
1004 |
|
1005 |
sub install_messages { |
428 |
sub install_messages { |
1006 |
my ($self) = @_; |
429 |
my ($self) = @_; |
1007 |
|
430 |
|
Lines 1012-1019
sub install_messages {
Link Here
|
1012 |
my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po"; |
435 |
my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po"; |
1013 |
|
436 |
|
1014 |
unless ( -f $pofile && -f $js_pofile ) { |
437 |
unless ( -f $pofile && -f $js_pofile ) { |
1015 |
$self->create_messages(); |
438 |
die "PO files for language '$self->{lang}' do not exist"; |
1016 |
} |
439 |
} |
|
|
440 |
|
1017 |
say "Install messages ($locale)" if $self->{verbose}; |
441 |
say "Install messages ($locale)" if $self->{verbose}; |
1018 |
make_path($modir); |
442 |
make_path($modir); |
1019 |
system "$self->{msgfmt} -o $mofile $pofile"; |
443 |
system "$self->{msgfmt} -o $mofile $pofile"; |
Lines 1035-1047
sub install_messages {
Link Here
|
1035 |
} |
459 |
} |
1036 |
} |
460 |
} |
1037 |
|
461 |
|
1038 |
sub remove_pot { |
|
|
1039 |
my $self = shift; |
1040 |
|
1041 |
unlink "$Bin/$self->{domain}.pot"; |
1042 |
unlink "$Bin/$self->{domain}-js.pot"; |
1043 |
} |
1044 |
|
1045 |
sub compress { |
462 |
sub compress { |
1046 |
my ($self, $files) = @_; |
463 |
my ($self, $files) = @_; |
1047 |
my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs(); |
464 |
my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs(); |
Lines 1074-1084
sub install {
Link Here
|
1074 |
my ($self, $files) = @_; |
491 |
my ($self, $files) = @_; |
1075 |
return unless $self->{lang}; |
492 |
return unless $self->{lang}; |
1076 |
$self->uncompress(); |
493 |
$self->uncompress(); |
1077 |
$self->install_tmpl($files) unless $self->{pref_only}; |
494 |
|
1078 |
$self->install_prefs(); |
495 |
if ($self->{pref_only}) { |
1079 |
$self->install_messages(); |
496 |
$self->install_prefs(); |
1080 |
$self->remove_pot(); |
497 |
} else { |
1081 |
$self->install_installer(); |
498 |
$self->install_tmpl($files); |
|
|
499 |
$self->install_prefs(); |
500 |
$self->install_messages(); |
501 |
$self->install_installer(); |
502 |
} |
1082 |
} |
503 |
} |
1083 |
|
504 |
|
1084 |
|
505 |
|
Lines 1090-1123
sub get_all_langs {
Link Here
|
1090 |
@files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files; |
511 |
@files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files; |
1091 |
} |
512 |
} |
1092 |
|
513 |
|
1093 |
|
|
|
1094 |
sub update { |
1095 |
my ($self, $files) = @_; |
1096 |
my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs(); |
1097 |
for my $lang ( @langs ) { |
1098 |
$self->set_lang( $lang ); |
1099 |
$self->uncompress(); |
1100 |
$self->update_tmpl($files) unless $self->{pref_only}; |
1101 |
$self->update_prefs(); |
1102 |
$self->update_messages(); |
1103 |
$self->update_installer(); |
1104 |
} |
1105 |
$self->remove_pot(); |
1106 |
} |
1107 |
|
1108 |
|
1109 |
sub create { |
1110 |
my ($self, $files) = @_; |
1111 |
return unless $self->{lang}; |
1112 |
$self->create_tmpl($files) unless $self->{pref_only}; |
1113 |
$self->create_prefs(); |
1114 |
$self->create_messages(); |
1115 |
$self->remove_pot(); |
1116 |
$self->create_installer(); |
1117 |
} |
1118 |
|
1119 |
|
1120 |
|
1121 |
1; |
514 |
1; |
1122 |
|
515 |
|
1123 |
|
516 |
|