File Coverage

blib/lib/Log/Report/Lexicon/POT.pm
Criterion Covered Total %
statement 146 163 89.5
branch 48 84 57.1
condition 17 39 43.5
subroutine 26 29 89.6
pod 15 16 93.7
total 252 331 76.1


line stmt bran cond sub pod time code
1             # This code is part of Perl distribution Log-Report-Lexicon version 1.15.
2             # The POD got stripped from this file by OODoc version 3.05.
3             # For contributors see file ChangeLog.
4              
5             # This software is copyright (c) 2007-2025 by Mark Overmeer.
6              
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             # SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later
10              
11             #oodist: *** DO NOT USE THIS VERSION FOR PRODUCTION ***
12             #oodist: This file contains OODoc-style documentation which will get stripped
13             #oodist: during its release in the distribution. You can use this file for
14             #oodist: testing, however the code of this development version may be broken!
15             #oorestyle: old style disclaimer to be removed.
16             #oorestyle: not found P for method filename($filename)
17              
18             # This code is part of distribution Log-Report-Lexicon. Meta-POD processed
19             # with OODoc into POD and HTML manual-pages. See README.md
20             # Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
21              
22             package Log::Report::Lexicon::POT;{
23             our $VERSION = '1.15';
24             }
25              
26 4     4   355036 use base 'Log::Report::Lexicon::Table';
  4         8  
  4         1408  
27              
28 4     4   25 use warnings;
  4         7  
  4         174  
29 4     4   18 use strict;
  4         10  
  4         89  
30              
31 4     4   18 use Log::Report 'log-report-lexicon';
  4         6  
  4         37  
32 4     4   2244 use Log::Report::Lexicon::PO ();
  4         12  
  4         133  
33              
34 4     4   23 use POSIX qw/strftime/;
  4         7  
  4         34  
35 4     4   356 use List::Util qw/sum/;
  4         8  
  4         323  
36 4     4   25 use Scalar::Util qw/blessed/;
  4         7  
  4         239  
37 4     4   21 use Encode qw/decode/;
  4         7  
  4         184  
38              
39 4     4   27 use constant MSGID_HEADER => '';
  4         6  
  4         10676  
40              
41             #--------------------
42              
43             sub init($)
44 2     2 0 6 { my ($self, $args) = @_;
45              
46 2         28 $self->{LRLP_fn} = $args->{filename};
47 2   50     35 $self->{LRLP_index} = $args->{index} || {};
48 2   50     11 $self->{LRLP_charset} = $args->{charset} || 'UTF-8';
49              
50 2         6 my $version = $args->{version};
51             my $domain = $args->{textdomain}
52 2 50       9 or error __"textdomain parameter is required";
53              
54 2         4 my $forms = $args->{plural_forms};
55 2 50       7 unless($forms)
56 2   50     39 { my $nrplurals = $args->{nr_plurals} || 2;
57 2   50     10 my $algo = $args->{plural_alg} || 'n!=1';
58 2         9 $forms = "nplurals=$nrplurals; plural=($algo);";
59             }
60              
61             $self->_createHeader(
62             project => $domain . (defined $version ? " $version" : ''),
63             forms => $forms,
64             charset => $args->{charset},
65             date => $args->{date}
66 2 50       30 );
67              
68 2         21 $self->setupPluralAlgorithm;
69 2         8 $self;
70             }
71              
72              
73             sub read($@)
74 3     3 1 3104 { my ($class, $fn, %args) = @_;
75 3         18 my $self = bless {LRLP_index => {}}, $class;
76              
77 3         6 my $charset = $args{charset};
78 3 50 66     20 $charset = $1
79             if !$charset && $fn =~ m!\.([\w-]+)(?:\@[^/\\]+)?\.po$!i;
80              
81 3         6 my $fh;
82 3 100       9 if(defined $charset)
83 2 50   1   67 { open $fh, "<:encoding($charset):crlf", $fn
  1         653  
  1         14  
  1         6  
84             or fault __x"cannot read in {cs} from file {fn}", cs => $charset, fn => $fn;
85             }
86             else
87 1 50       76 { open $fh, '<:raw:crlf', $fn
88             or fault __x"cannot read from file {fn} (unknown charset)", fn=>$fn;
89             }
90              
91 3         1107 local $/ = "\n\n";
92 3         4 my $linenr = 1; # $/ frustrates $fh->input_line_number
93 3         5 while(1)
94 43         69 { my $location = "$fn line $linenr";
95 43         410 my $block = <$fh>;
96 43 100       136 defined $block or last;
97              
98 40         82 $linenr += $block =~ tr/\n//;
99              
100 40         256 $block =~ s/\s+\z//s;
101 40 50       77 length $block or last;
102              
103 40 100       62 unless($charset)
104 1 50       60 { $charset = $block =~ m/\"content-type:.*?charset=["']?([\w-]+)/mi ? $1
105             : error __x"cannot detect charset in {fn}", fn => $fn;
106              
107 1         12 trace "auto-detected charset $charset for $fn";
108 1         99 binmode $fh, ":encoding($charset):crlf";
109              
110 1 50       6334 $block = decode $charset, $block
111             or error __x"unsupported charset {charset} in {fn}", charset => $charset, fn => $fn;
112             }
113              
114 40         161 my $po = Log::Report::Lexicon::PO->fromText($block, $location);
115 40 50       97 $self->add($po) if $po;
116             }
117              
118 3 50       47 close $fh
119             or fault __x"failed reading from file {fn}", fn => $fn;
120              
121 3         7 $self->{LRLP_fn} = $fn;
122 3         5 $self->{LRLP_charset} = $charset;
123              
124 3         16 $self->setupPluralAlgorithm;
125 3         31 $self;
126             }
127              
128              
129              
130             sub write($@)
131 2     2 1 5 { my $self = shift;
132 2 100       15 my $file = @_%2 ? shift : $self->filename;
133 2         8 my %args = @_;
134              
135 2 50       7 defined $file
136             or error __"no filename or file-handle specified for PO";
137              
138 2         7 my $need_refs = $args{only_active};
139 2         22 my @opt = (nr_plurals => $self->nrPlurals);
140              
141 2         5 my $fh;
142 2 100       11 if(ref $file) { $fh = $file }
  1         2  
143             else
144 1         4 { my $layers = '>:encoding('.$self->charset.')';
145 1 50       194 open $fh, $layers, $file
146             or fault __x"cannot write to file {fn} with {layers}", fn => $file, layers => $layers;
147             }
148              
149 2         116 $fh->print($self->msgid(MSGID_HEADER)->toString(@opt));
150 2         11694 my $index = $self->index;
151 2         28 foreach my $msgid (sort keys %$index)
152 15 100       202 { next if $msgid eq MSGID_HEADER;
153              
154 13         31 my $rec = $index->{$msgid};
155             my @recs = blessed $rec ? $rec # one record with $msgid
156 13 50       91 : @{$rec}{sort keys %$rec}; # multiple records, msgctxt
  0         0  
157              
158 13         29 foreach my $po (@recs)
159 13 50       42 { next if $po->useless;
160 13 50 33     39 next if $need_refs && !$po->references;
161 13         35 $fh->print("\n", $po->toString(@opt));
162             }
163             }
164              
165             $fh->close
166 2 50       40 or failure __x"write errors for file {fn}", fn => $file;
167              
168 2         104 $self;
169             }
170              
171             #--------------------
172              
173 1     1 1 23 sub charset() { $_[0]->{LRLP_charset} }
174 111     111 1 384 sub index() { $_[0]->{LRLP_index} }
175 1     1 1 4 sub filename() { $_[0]->{LRLP_fn} }
176              
177              
178 0 0   0 1 0 sub language() { $_[0]->filename =~ m![/\\](\w+)[^/\\]*$! ? $1 : undef }
179              
180             #--------------------
181              
182             sub msgid($;$)
183 50     50 1 129 { my ($self, $msgid, $msgctxt) = @_;
184 50 100       140 my $msgs = $self->index->{$msgid} or return;
185              
186 37 50 33     268 return $msgs
      33        
187             if blessed $msgs
188             && (!$msgctxt || $msgctxt eq $msgs->msgctxt);
189              
190 0         0 $msgs->{$msgctxt};
191             }
192              
193              
194             sub msgstr($;$$)
195 15     15 1 82 { my ($self, $msgid, $count, $msgctxt) = @_;
196 15 50       48 my $po = $self->msgid($msgid, $msgctxt)
197             or return undef;
198              
199 15   100     46 $count //= 1;
200 15         62 $po->msgstr($self->pluralIndex($count));
201             }
202              
203              
204             sub add($)
205 53     53 1 94 { my ($self, $po) = @_;
206 53         116 my $msgid = $po->msgid;
207 53         98 my $index = $self->index;
208              
209 53         85 my $h = $index->{$msgid};
210 53 50       262 $h or return $index->{$msgid} = $po;
211              
212 0 0 0     0 $h = $index->{$msgid} = +{ ($h->msgctxt // '') => $h }
213             if blessed $h;
214              
215 0   0     0 my $ctxt = $po->msgctxt // '';
216             error __x"translation already exists for '{msgid}' with '{ctxt}", msgid => $msgid, ctxt => $ctxt
217 0 0       0 if $h->{$ctxt};
218              
219 0         0 $h->{$ctxt} = $po;
220             }
221              
222              
223             sub translations(;$)
224 5     5 1 997 { my $self = shift;
225 5 50       15 @_ or return map +(blessed $_ ? $_ : values %$_), values %{$self->index};
  4 100       11  
226              
227 1 50       4 error __x"the only acceptable parameter is 'ACTIVE', not '{p}'", p => $_[0]
228             if $_[0] ne 'ACTIVE';
229              
230 1         4 grep $_->isActive, $self->translations;
231             }
232              
233              
234 2     2   114 sub _now() { strftime "%Y-%m-%d %H:%M%z", localtime }
235              
236             sub header($;$)
237 13     13 1 689 { my ($self, $field) = (shift, shift);
238 13 50       49 my $header = $self->msgid(MSGID_HEADER)
239             or error __x"no header defined in POT for file {fn}", fn => $self->filename;
240              
241 13 100       34 if(!@_)
242 8   50     30 { my $text = $header->msgstr(0) || '';
243 8 100       512 return $text =~ m/^\Q$field\E\:\s*([^\n]*?)\;?\s*$/im ? $1 : undef;
244             }
245              
246 5         11 my $content = shift;
247 5         23 my $text = $header->msgstr(0);
248              
249 5         31 for($text)
250 5 100       16 { if(defined $content)
251 4 100       192 { s/^\Q$field\E\:([^\n]*)/$field: $content/im # change
252             || s/\z/$field: $content\n/; # new
253             }
254             else
255 1         40 { s/^\Q$field\E\:[^\n]*\n?//im; # remove
256             }
257             }
258              
259 5         31 $header->msgstr(0, $text);
260 5         23 $content;
261             }
262              
263              
264             sub updated(;$)
265 2     2 1 7 { my $self = shift;
266 2   66     13 my $date = shift || _now;
267 2         10 $self->header('PO-Revision-Date', $date);
268 2         5 $date;
269             }
270              
271             ### internal
272             sub _createHeader(%)
273 2     2   14 { my ($self, %args) = @_;
274 2   66     26 my $date = $args{date} || _now;
275              
276 2         26 my $header = Log::Report::Lexicon::PO->new(msgid => MSGID_HEADER, msgstr => <<__CONFIG);
277             Project-Id-Version: $args{project}
278             Report-Msgid-Bugs-To:
279             POT-Creation-Date: $date
280             PO-Revision-Date: $date
281             Last-Translator:
282             Language-Team:
283             MIME-Version: 1.0
284             Content-Type: text/plain; charset=$args{charset}
285             Content-Transfer-Encoding: 8bit
286             Plural-Forms: $args{forms}
287             __CONFIG
288              
289 2   50     12 my $version = $Log::Report::VERSION || '0.0';
290 2         11 $header->addAutomatic("Header generated with ".__PACKAGE__." $version\n");
291              
292 2 50       12 $self->index->{&MSGID_HEADER} = $header
293             if $header;
294              
295 2         20 $header;
296             }
297              
298              
299             sub removeReferencesTo($)
300 1     1 1 4 { my ($self, $filename) = @_;
301 1         5 sum map $_->removeReferencesTo($filename), $self->translations;
302             }
303              
304              
305             sub keepReferencesTo($)
306 0     0 1 0 { my ($self, $keep) = @_;
307 0         0 sum map $_->keepReferencesTo($keep), $self->translations;
308             }
309              
310              
311             sub stats()
312 0     0 1 0 { my $self = shift;
313 0         0 my %stats = (msgids => 0, fuzzy => 0, inactive => 0);
314 0         0 foreach my $po ($self->translations)
315 0 0       0 { next if $po->msgid eq MSGID_HEADER;
316 0         0 $stats{msgids}++;
317 0 0       0 $stats{fuzzy}++ if $po->fuzzy;
318 0 0 0     0 $stats{inactive}++ if !$po->isActive && !$po->useless;
319             }
320 0         0 \%stats;
321             }
322              
323             1;