File Coverage

blib/lib/Locale/MakePhrase/BackingStore/Directory.pm
Criterion Covered Total %
statement 144 178 80.9
branch 64 126 50.7
condition 18 45 40.0
subroutine 14 15 93.3
pod 2 2 100.0
total 242 366 66.1


line stmt bran cond sub pod time code
1             package Locale::MakePhrase::BackingStore::Directory;
2             our $VERSION = 0.3;
3             our $DEBUG = 0;
4              
5             =head1 NAME
6              
7             Locale::MakePhrase::BackingStore::Directory - Retrieve translations
8             from files located in a specified directory.
9              
10             =head1 DESCRTIPION
11              
12             This backing store is capable of loading language rules, from files
13             located in the specified directory. All files ending with the
14             extension B<.mpt> will try to be loaded.
15              
16             Files need to be named according to language/dialect. For example:
17              
18             en.mpt
19             en_au.mpt
20             cn.mpt
21              
22             Thus, the filename is used to defined the I component of
23             the language rule object.
24              
25             The files must be formatted as shown in the B and
26             B files (which can be located in the same directories
27             that these modules are are installed in). The important points to
28             note are that the file is broken into groups containing:
29              
30             =over 2
31              
32             =item B
33              
34             =item B
35              
36             =item B
37              
38             =item B
39              
40             Where expression & priority are optional. However, if you specify the
41             priority and/or expression, make sure the translation key is the last
42             entry in the group - this is necessary, as we dont know when the
43             the block is finished.
44              
45             =back
46              
47             =head1 API
48              
49             The following methods are implemented:
50              
51             =cut
52              
53 2     2   2090 use strict;
  2         3  
  2         76  
54 2     2   10 use warnings;
  2         4  
  2         55  
55 2     2   9 use utf8;
  2         5  
  2         15  
56 2     2   50 use Data::Dumper;
  2         3  
  2         106  
57 2     2   18 use base qw(Locale::MakePhrase::BackingStore);
  2         11  
  2         186  
58 2     2   9 use I18N::LangTags;
  2         4  
  2         98  
59 2     2   12 use Locale::MakePhrase::Utils qw(alltrim die_from_caller);
  2         3  
  2         6163  
60             our $implicit_data_structure = [ "key","expression","priority","translation" ];
61             our $language_file_extension = '.mpt'; # .mpt => 'MakePhrase Translations'
62             our $default_encoding = 'utf-8';
63             local $Data::Dumper::Indent = 1 if $DEBUG;
64              
65             #--------------------------------------------------------------------------
66              
67             =head2 $self new([...])
68              
69             We support loading text/translations (from the translation files) which
70             may be encoded using any character encoding. Since we need to know
71             something about the files we are trying to load, we expect this object
72             to be constructed with the following options:
73              
74             =over 2
75              
76             =item C
77              
78             The full path to the directory containing the translation files. eg:
79              
80             /usr/local/myapp/translations
81              
82             Default: none; you must specify a directory
83              
84             =item C
85              
86             You can specify a different file extension to use, rather than using
87             B<.mpt>.
88              
89             Default: use B<.mpt> as the extension
90              
91             =item C
92              
93             We can load translations from any enocding supported by the L
94             module. Upon load, this module will convert the translations from
95             the specified encoding, into the interal encoding of UTF-8.
96              
97             Default: load UTF-8 text translations.
98              
99             =item C
100              
101             This module will dynamically reload its known translations, if the
102             files get updated. You can set this option to avoid reloading the
103             file if it changes.
104              
105             Default: reload language files if changed
106              
107             =back
108              
109             =cut
110              
111             sub new {
112 1     1 1 500 my $proto = shift;
113 1   33     11 my $class = ref($proto) || $proto;
114 1         6 my $self = bless {}, $class;
115              
116             # get options
117 1         2 my %options;
118 1 50 33     11 if (@_ > 1 and not(@_ % 2)) {
    0 0        
    0          
119 1         5 %options = @_;
120             } elsif (@_ == 1 and ref($_[0]) eq 'HASH') {
121 0         0 %options = %{$_[0]};
  0         0  
122             } elsif (@_ == 1) {
123 0         0 $options{directory} = shift;
124             }
125 1 50       10 print STDERR "Arguments to ". ref($self) .": ". Dumper(\%options) if $DEBUG > 5;
126              
127             # allow sub-class to control construction
128 1         14 $self = $self->init();
129 1 50       9 return undef unless $self;
130              
131 1 50       6 $self->{directory} = (exists $options{directory}) ? $options{directory} : $self->{directory};
132 1 50       8 $self->{extension} = (exists $options{extension}) ? $options{extension} : (exists $self->{extension}) ? $self->{extension} : $language_file_extension;
    50          
133 1 50       7 $self->{encoding} = (exists $options{encoding}) ? $options{encoding} : (exists $self->{encoding}) ? $self->{encoding} : $default_encoding;
    50          
134 1 0       11 $self->{reload} = (exists $options{reload}) ? ($options{reload} ? 1 : 0) : (exists $self->{reload}) ? ($self->{reload} ? 1 : 0) : 1;
    0          
    50          
    50          
135 1         4 $self->{loaded_languages} = {};
136 1         3 $self->{rules} = {};
137              
138             # Error checking
139 1 50       5 die_from_caller("Missing 'directory' definition") unless (defined $self->{directory});
140 1 50       33 die_from_caller("No such directory:",$self->{directory}) unless (-d $self->{directory});
141 1 50       4 die_from_caller("Invalid encoding specified") unless $self->{encoding};
142 1 50       5 die_from_caller("Invalid file extension") unless (defined $self->{extension});
143              
144             # check the file extension
145 1 50 33     12 if (length $self->{extension} and substr($self->{extension},0,1) ne '.') {
146 0         0 $self->{extension} = ".".$self->{extension};
147             }
148              
149             # Pre-load all available languages
150 1         7 $self->_load_language_files();
151              
152 1         5 return $self;
153             }
154              
155             #--------------------------------------------------------------------------
156              
157             =head2 \@rule_objs get_rules($context,$key,\@languages)
158              
159             Retrieve the translations (that have been previously loaded), using
160             the selected languages. This implementation will reload the
161             appropiate language file if it changes (unless it has been told not
162             to).
163              
164             =cut
165              
166             sub get_rules {
167 3     3 1 6 my ($self,$context,$key,$languages) = @_;
168 3         4 my @translations;
169              
170             # make sure languages are loaded
171 3 50       14 $self->_load_languages($languages) if $self->{reload};
172              
173             # look for rules for each language in the current key
174 3         5 my @langs;
175 3         6 my $rules = $self->{rules};
176 3         7 foreach my $language (@$languages) {
177 6 100       17 next unless (exists $rules->{$language});
178 3         7 push @langs, $rules->{$language};
179             }
180 3 50       9 return undef unless @langs;
181 3         6 $rules = undef;
182              
183             # Only use rules which match this context, if we are using a context
184 3 50       7 if ($context) {
185              
186             # look for rules that match the key
187 0         0 foreach my $language (@langs) {
188 0         0 my $keys = $language->{$key};
189 0 0 0     0 next unless ($keys or ref($keys) ne 'HASH');
190 0         0 $keys = $keys->{$context};
191 0 0       0 next unless $keys;
192 0         0 foreach my $ky (@$keys) {
193 0         0 push @translations, $ky;
194             }
195             }
196              
197             } else {
198              
199             # look for rules that match the key
200 3         6 foreach my $language (@langs) {
201 3         5 my $keys = $language->{$key};
202 3 50       10 next unless $keys;
203 3         4 $keys = $keys->{_};
204 3         6 foreach my $ky (@$keys) {
205 5         15 push @translations, $ky;
206             }
207             }
208              
209             }
210              
211 3 50       10 print STDERR "Found translations:\n", Dumper(@translations) if $DEBUG;
212 3         12 return \@translations;
213             }
214              
215             #--------------------------------------------------------------------------
216             # The following methods are not part of the API - they are private.
217             #
218             # This means that everything above this code-break is allowed/designed
219             # to be overloaded.
220             #--------------------------------------------------------------------------
221              
222             #--------------------------------------------------------------------------
223             #
224             # Load all the available language files
225             #
226             sub _load_language_files {
227 1     1   2 my ($self) = @_;
228 1         4 my $dir = $self->{directory};
229 1         3 my $ext = $self->{extension};
230 1 50       18 die_from_caller("Directory is not readable:",$dir) unless (-r $dir);
231 1 50       49 opendir(DIR, $dir) or die_from_caller("Failed to read into directory:",$dir);
232 1         27 my @files = readdir(DIR);
233 1         15 closedir DIR;
234 1         6 foreach my $language (@files) {
235 4 100       73 next unless ($language =~ /$ext$/);
236 2 50 33     81 next unless ((-f "$dir/$language" || -l "$dir/$language") and -r "$dir/$language");
      33        
237 2         18 $language =~ s/$ext$//;
238 2 50       11 next unless I18N::LangTags::is_language_tag($language);
239 0         0 $self->_load_language($language);
240             }
241             }
242              
243             #--------------------------------------------------------------------------
244             #
245             # Load the translations for each language.
246             #
247             # If the file for that language hasn't yet been loaded or its mtime has changed,
248             # load it into the cache.
249             #
250             # If the cached language is valid, dont do anything.
251             #
252             sub _load_languages {
253 3     3   6 my ($self,$languages) = @_;
254 3         4 my $loaded_languages = $self->{loaded_languages};
255 3         7 my $rules = $self->{rules};
256 3         8 foreach my $language (@$languages) {
257 6 100       18 if (exists $loaded_languages->{$language}) {
258 2         5 my $file = $loaded_languages->{$language}->{file};
259 2         35 my $mtime = (stat($file))[9];
260 2 50       11 next if ($loaded_languages->{$language}->{mtime} == $mtime);
261 0         0 $rules->{$language} = undef;
262             }
263 4         11 $self->_load_language($language);
264             }
265             }
266              
267             #--------------------------------------------------------------------------
268             #
269             # Load the translations for the language.
270             #
271             sub _load_language {
272 4     4   34 my ($self,$language) = @_;
273              
274             # get the name of the language file, then open it
275 4         7 my $file;
276 4 50       12 if (exists $self->{loaded_languages}->{$language}) {
277 0         0 $file = $self->{loaded_languages}->{$language}->{file};
278             }
279 4 50       11 unless (defined $file) {
280 4         14 $file = $self->_get_language_filename($language);
281 4 100       18 return unless (defined $file);
282 1         6 $self->{loaded_languages}->{$language}->{file} = $file;
283             }
284 1         25 $self->{loaded_languages}->{$language}->{mtime} = (stat($file))[9];
285              
286             # Load the translations from the file (skip empty lines, or comments)
287 1         7 my $rules = $self->{rules}->{$language};
288 1 50       5 $rules = {} unless $rules;
289 1         3 my ($key,$expression,$priority,$translation,$context);
290 1         12 my $in_group = 0;
291 1         2 my $line = 0;
292 1         3 my $encoding = $self->{encoding};
293 1         2 my $fh;
294 1 50   1   54 open ($fh, "<:encoding($encoding)", "$file") || return;
  1         1300  
  1         11  
  1         7  
295              
296 1         1752 while (<$fh>) {
297 12         41 chomp;
298 12         21 s/ $//;
299 12         15 $line++;
300 12         34 $_ = alltrim($_);
301 12 100 66     104 next if (not defined or length == 0 or /^#/);
      66        
302              
303             # search for group entries
304 8         29 /^
305             ([^=]*)=(.*)
306             |
307             (?:.+)
308             $/sx;
309 8 50       24 next unless ($1);
310 8         22 my $lhs = alltrim($1);
311 8         26 my $rhs = alltrim($2);
312              
313             # process group entries
314 8 100 66     61 if ($lhs eq 'key') {
    100 66        
    100 33        
    50 0        
    0          
315 3 50       153 die_from_caller("Found another group while processing previous group, file '$file' line '$line'") if $in_group;
316 3         4 $in_group++;
317 3         5 $key = $rhs;
318 3 50       9 die_from_caller("Key must have some length, file '$file' line '$line'") unless (length $key);
319             # $line += _read_lines($fh,\$key);
320 3         14 next;
321             } elsif ($lhs eq 'expression' and not defined $expression) {
322 1         2 $expression = $rhs;
323             } elsif ($lhs eq 'priority' and not defined $priority) {
324 1         2 $priority = $rhs;
325 1         4 $priority = int($priority); # must be a valid number
326             } elsif ($lhs eq 'translation' and not defined $translation) {
327 3         5 $translation = $rhs;
328 3 50       9 die_from_caller("Translation must have some length, file '$file' line '$line'") unless (length $translation);
329             # $line += _read_lines($fh,\$translation);
330             } elsif ($lhs eq 'context' and not defined $context) {
331 0         0 $context = $rhs;
332             } else {
333 0         0 die_from_caller("Syntax error in translation file '$file', line '$line'");
334             }
335              
336             # Have we enough info to make a linguistic rule?
337 5 100       17 next unless (defined $translation);
338 3 100       8 $expression = "" unless $expression;
339 3 100       23 $priority = 0 unless $priority;
340 3 50       9 $context = "" unless $context;
341              
342             # Make this linguistic rule, and add it to any others that may exist for this language/key
343 3         5 $in_group--;
344 3         3 my $entries;
345 3 50       7 if ($context) {
346 0         0 $entries = $rules->{$key}{$context};
347 0 0       0 unless ($entries) {
348 0 0       0 $entries = [] unless $entries;
349 0         0 $rules->{$key}{$context} = $entries;
350             }
351             } else {
352 3         11 $entries = $rules->{$key}{_};
353 3 100       8 unless ($entries) {
354 2 50       6 $entries = [] unless $entries;
355 2         7 $rules->{$key}{_} = $entries;
356             }
357             }
358 3         111 push @$entries, $self->make_rule(
359             key => $key,
360             language => $language,
361             expression => $expression,
362             priority => $priority,
363             translation => $translation,
364             );
365              
366 3         18 $key = $expression = $priority = $translation = $context = undef;
367             }
368              
369 1         19 close $fh;
370 1         14 $self->{rules}->{$language} = $rules;
371             }
372              
373             #--------------------------------------------------------------------------
374             #
375             # Helper routine for looking up filenames for a given language
376             #
377             sub _get_language_filename {
378 4     4   7 my ($self, $language) = @_;
379 4         14 my $path = $self->{directory} ."/". $language . $self->{extension};
380 4 100 66     118 if ((-f $path || -l $path) and -r $path) {
      66        
381 1 50       5 print STDERR "Found new language file: $path" if $DEBUG > 2;
382 1         4 return $path;
383             }
384 3         6 return undef;
385             }
386              
387             #--------------------------------------------------------------------------
388             #
389             # Helper routine for reading multiple lines for a given key
390             #
391             sub _read_lines {
392 0     0   0 my ($fh,$s_ref);
393 0         0 my $line = 0;
394 0 0       0 if ($$s_ref =~ /\/$/) {
395 0         0 while (<$fh>) {
396 0         0 chomp;
397 0         0 s/ $//;
398 0         0 $line++;
399 0         0 $_ = alltrim($_);
400 0 0       0 if (/\.\s*\\$/) {
401 0         0 $$s_ref =~ s/\s*\/$/\n/;
402             } else {
403 0         0 $$s_ref =~ s/\s*\/$/ /;
404             }
405 0         0 $$s_ref .= $_;
406 0 0       0 last unless ($$s_ref =~ /\/$/);
407             }
408             }
409 0         0 return $line;
410             }
411              
412             1;
413             __END__