File Coverage

blib/lib/Locale/MakePhrase/BackingStore/File.pm
Criterion Covered Total %
statement 118 148 79.7
branch 51 104 49.0
condition 18 42 42.8
subroutine 11 12 91.6
pod 2 2 100.0
total 200 308 64.9


line stmt bran cond sub pod time code
1             package Locale::MakePhrase::BackingStore::File;
2             our $VERSION = 0.3;
3             our $DEBUG = 0;
4              
5             =head1 NAME
6              
7             Locale::MakePhrase::BackingStore::File - Retrieve language
8             translations for all supported languages, from a single file.
9              
10             =head1 DESCRIPTION
11              
12             This backing store is capable of loading language rules from a
13             single translation file.
14              
15             The file must be formatted as shown in the B file (which
16             can be located in the same directories that these modules are are
17             installed in). The important points to note are that the file is
18             broken into groups containing:
19              
20             =over 2
21              
22             =item B
23              
24             =item B
25              
26             =item B
27              
28             =item B
29              
30             =item B
31              
32             Where expression & priority are optional. However, if you specify the
33             priority and/or expression, make sure the translation key is the last
34             entry in the group - this is necessary, as we dont know when the
35             the block is finished.
36              
37             =back
38              
39             =head1 API
40              
41             The following methods are implemented:
42              
43             =cut
44              
45 6     6   14317 use strict;
  6         16  
  6         278  
46 6     6   41 use warnings;
  6         15  
  6         314  
47 6     6   37 use utf8;
  6         13  
  6         78  
48 6     6   165 use Data::Dumper;
  6         12  
  6         505  
49 6     6   56 use base qw(Locale::MakePhrase::BackingStore);
  6         11  
  6         687  
50 6     6   1718 use I18N::LangTags;
  6         8428  
  6         305  
51 6     6   80 use Locale::MakePhrase::Utils qw(die_from_caller alltrim);
  6         14  
  6         11967  
52             our $implicit_data_structure = [ "key","language","expression","priority","translation" ];
53             our $default_encoding = 'utf-8';
54             local $Data::Dumper::Indent = 1 if $DEBUG;
55              
56             #--------------------------------------------------------------------------
57              
58             =head2 $self new([...])
59              
60             We support loading text/translations (from the translation file) which
61             may be encoded using any character encoding. Since we need to know
62             something about the file we are trying to load, we expect this object
63             to be constructed with the following options:
64              
65             =over 2
66              
67             =item C
68              
69             The full path to the file containing the translations. eg:
70              
71             /usr/local/myapp/translations.mpt
72              
73             Default: none; you must specify a filename
74              
75             =item C
76              
77             We can load translations from any enocding supported by the L
78             module. Upon load, this module will convert the translations from
79             the specified encoding, into the interal encoding of UTF-8.
80              
81             Default: load UTF-8 encoded text translations.
82              
83             =item C
84              
85             It is handy for the language module to be able to dynamically reload
86             its known translations, if the file gets updated. You can set this
87             to avoid reloading the file if it changes.
88              
89             Default: reload if file changes
90              
91             =back
92              
93             =cut
94              
95             sub new {
96 5     5 1 2100 my $proto = shift;
97 5   33     42 my $class = ref($proto) || $proto;
98 5         27 my $self = bless {}, $class;
99              
100             # get options
101 5         13 my %options;
102 5 50 33     65 if (@_ > 1 and not(@_ % 2)) {
    0 0        
    0          
103 5         22 %options = @_;
104             } elsif (@_ == 1 and ref($_[0]) eq 'HASH') {
105 0         0 %options = %{$_[0]};
  0         0  
106             } elsif (@_ == 1) {
107 0         0 $options{file} = shift;
108             }
109 5 50       22 print STDERR "Arguments to ". ref($self) .": ". Dumper(\%options) if $DEBUG > 5;
110              
111             # allow sub-class to control construction
112 5         71 $self = $self->init();
113 5 50       47 return undef unless $self;
114              
115 5 50       27 $self->{file} = (exists $options{file}) ? $options{file} : $self->{file};
116 5 50       36 $self->{encoding} = (exists $options{encoding}) ? $options{encoding} : (exists $self->{encoding}) ? $self->{encoding} : $default_encoding;
    50          
117 5 0       45 $self->{reload} = (exists $options{reload}) ? ($options{reload} ? 1 : 0) : (exists $self->{reload}) ? ($self->{reload} ? 1 : 0) : 1;
    0          
    50          
    50          
118 5         13 $self->{rules} = {};
119 5         16 $self->{mtime} = 0;
120              
121             # make sure file exists
122 5 50       19 die_from_caller("Missing 'file' definition") unless (defined $self->{file});
123 5 50       161 die_from_caller("No such translation file:",$self->{file}) unless (-e $self->{file});
124 5 50       21 die_from_caller("Invalid encoding specified") unless $self->{encoding};
125              
126             # Pre-load all available languages
127 5         25 $self->_load_file();
128              
129 5         34 return $self;
130             }
131              
132             #--------------------------------------------------------------------------
133              
134             =head2 \@rule_objs get_rules($context,$key,\@languages)
135              
136             Retrieve the translations (that have been previously loaded), using the
137             selected languages. This implementation will reload the language file
138             if it changes (unless it has been told not to).
139              
140             =cut
141              
142             sub get_rules {
143 23     23 1 37 my ($self,$context,$key,$languages) = @_;
144 23         28 my @translations;
145              
146             # make sure languages are loaded
147 23 50       84 $self->_load_file() unless $self->{dont_reload};
148              
149             # look for rules for each language in the current key
150 23         27 my @langs;
151 23         35 my $rules = $self->{rules};
152 23         42 foreach my $language (@$languages) {
153 46 50       107 next unless (exists $rules->{$language});
154 46         99 push @langs, $rules->{$language};
155             }
156 23 50       58 return undef unless @langs;
157 23         35 $rules = undef;
158              
159             # Only use rules which match this context, if we are using a context
160 23 50       42 if ($context) {
161              
162             # look for rules that match the key
163 0         0 foreach my $language (@langs) {
164 0         0 my $keys = $language->{$key};
165 0 0 0     0 next unless ($keys or ref($keys) ne 'HASH');
166 0         0 $keys = $keys->{$context};
167 0 0       0 next unless $keys;
168 0         0 foreach my $ky (@$keys) {
169 0         0 push @translations, $ky;
170             }
171             }
172              
173             } else {
174              
175             # look for rules that match the key
176 23         32 foreach my $language (@langs) {
177 46         76 my $keys = $language->{$key};
178 46 100       114 next unless $keys;
179 14         21 $keys = $keys->{_};
180 14         23 foreach my $ky (@$keys) {
181 28         70 push @translations, $ky;
182             }
183             }
184              
185             }
186              
187 23 50       100 print STDERR "Found translations:\n", Dumper(@translations) if $DEBUG;
188 23         90 return \@translations;
189             }
190              
191             #--------------------------------------------------------------------------
192             # The following methods are not part of the API - they are private.
193             #
194             # This means that everything above this code-break is allowed/designed
195             # to be overloaded.
196             #--------------------------------------------------------------------------
197              
198             #--------------------------------------------------------------------------
199             #
200             # If the file hasn't yet been loaded or its mtime has changed, load it into the cache.
201             #
202             sub _load_file {
203 28     28   41 my ($self) = @_;
204 28         45 my $file = $self->{file};
205 28 50 33     945 die_from_caller("Incorrect permissions on translation file:",$file) unless ((-f $file || -l $file) and -r $file);
      33        
206              
207             # if mtime is same as previous, do nothing
208 28         397 my $mtime = (stat($file))[9];
209 28 100       121 return if ($mtime == $self->{mtime});
210              
211             # ... mtime has changed -> reload the file
212 5         18 $self->{mtime} = $mtime;
213 5         14 $self->{rules} = undef;
214              
215             # Load the translations from the file (skip empty lines, or comments)
216 5         13 my $rules = {};
217 5         11 my ($key,$language,$expression,$priority,$translation,$context);
218 5         9 my $in_group = 0;
219 5         11 my $line = 0;
220 5         10 my $encoding = $self->{encoding};
221 5         10 my $fh;
222 5 50   5   242 open ($fh, "<:encoding($encoding)", "$file") || die_from_caller("Failed to open translation file:",$file);
  5         5790  
  5         61  
  5         28  
223              
224 5         8196 while (<$fh>) {
225 355         757 chomp;
226 355         524 s/ //;
227 355         351 $line++;
228 355         824 $_ = alltrim($_);
229 355 100 66     2563 next if (not defined or length == 0 or /^#/);
      100        
230              
231             # search for group entries
232 210         634 /^
233             ([^=]*)=(.*)
234             |
235             (.*)
236             $/sx;
237 210 50       560 next unless ($1);
238 210         513 my $lhs = alltrim($1);
239 210         496 my $rhs = alltrim($2);
240              
241             # process group entries
242 210 100 66     1287 if ($lhs eq 'key') {
    100 66        
    100 66        
    100 33        
    50 0        
    0          
243 50 50       117 die_from_caller("Found another group while processing previous group, file '$file' line '$line'") if ($in_group);
244 50         53 $in_group++;
245 50         68 $key = $rhs;
246 50 50       104 die_from_caller("Key must have some length, file '$file' line '$line'") unless (length $key);
247             # $line += _read_lines($fh,\$key);
248 50         306 next;
249             } elsif ($lhs eq 'language' and not defined $language) {
250 50         78 $language = $rhs;
251 50         87 $language =~ tr<_A-Z><-a-z>; # support the variations case/hyphenation for language/locale
252 50 50       108 die_from_caller("Language must have some length, file '$file' line '$line'") unless (length $language);
253 50 50       149 die_from_caller("Must be valid language tag, file '$file' line '$line'") unless (I18N::LangTags::is_language_tag($language));
254 50         550 $language =~ tr<-><_>;
255             } elsif ($lhs eq 'expression' and not defined $expression) {
256 30         44 $expression = $rhs;
257             } elsif ($lhs eq 'priority' and not defined $priority) {
258 30         46 $priority = $rhs;
259 30         58 $priority = int($priority); # must be a valid number
260             } elsif ($lhs eq 'translation' and not defined $translation) {
261 50         70 $translation = $rhs;
262 50 50       118 die_from_caller("Translation must have some length, file '$file' line '$line'") unless (length $translation);
263             # $line += _read_lines($fh,\$translation);
264             } elsif ($lhs eq 'context' and not defined $context) {
265 0         0 $context = $rhs;
266             } else {
267 0         0 die_from_caller("Syntax error in translation file '$file', line '$line'");
268             }
269              
270             # Have we enough info to make a linguistic rule?
271 160 100 66     902 next unless (defined $language and $translation);
272 50 100       106 $expression = "" unless $expression;
273 50 100       87 $priority = 0 unless $priority;
274 50 50       91 $context = "" unless $context;
275              
276             # Make this linguistic rule, and add it to any others that may exist for this language/key
277 50         55 $in_group--;
278 50         48 my $entries;
279 50 50       90 if ($context) {
280 0         0 $entries = $rules->{$language}{$key}{$context};
281 0 0       0 unless ($entries) {
282 0 0       0 $entries = [] unless $entries;
283 0         0 $rules->{$language}{$key}{$context} = $entries;
284             }
285             } else {
286 50         182 $entries = $rules->{$language}{$key}{_};
287 50 100       110 unless ($entries) {
288 30 50       108 $entries = [] unless $entries;
289 30         99 $rules->{$language}{$key}{_} = $entries;
290             }
291             }
292 50         227 push @$entries, $self->make_rule(
293             key => $key,
294             language => $language,
295             expression => $expression,
296             priority => $priority,
297             translation => $translation,
298             );
299              
300 50         278 $key = $language = $expression = $priority = $translation = $context = undef;
301             }
302              
303 5         173 close $fh;
304 5         57 $self->{rules} = $rules;
305 5 50       53 print STDERR "Loaded the following languages rules:\n", Dumper($rules) if $DEBUG > 7;
306             }
307              
308             #--------------------------------------------------------------------------
309             #
310             # Helper routine for reading multiple lines for a given key
311             #
312             sub _read_lines {
313 0     0   0 my ($fh,$s_ref);
314 0         0 my $line = 0;
315 0 0       0 if ($$s_ref =~ /\/$/) {
316 0         0 while (<$fh>) {
317 0         0 chomp;
318 0         0 s/^M$//;
319 0         0 $line++;
320 0         0 $_ = alltrim($_);
321 0 0       0 if (/\.\s*\\$/) {
322 0         0 $$s_ref =~ s/\s*\/$/\n/;
323             } else {
324 0         0 $$s_ref =~ s/\s*\/$/ /;
325             }
326 0         0 $$s_ref .= $_;
327 0 0       0 last unless ($$s_ref =~ /\/$/);
328             }
329             }
330 0         0 return $line;
331             }
332              
333             1;
334             __END__