File Coverage

lib/Class/Usul/L10N.pm
Criterion Covered Total %
statement 61 76 80.2
branch 12 18 66.6
condition 10 20 50.0
subroutine 18 24 75.0
pod 5 5 100.0
total 106 143 74.1


line stmt bran cond sub pod time code
1             package Class::Usul::L10N;
2              
3 3     3   189882 use namespace::autoclean;
  3         74752  
  3         26  
4              
5 3     3   1008 use Class::Null;
  3         475  
  3         128  
6 3     3   1032 use Class::Usul::Constants qw( FALSE LANG NUL SEP TRUE );
  3         10  
  3         32  
7 3         30 use Class::Usul::Functions qw( assert is_arrayref
8 3     3   3962 is_hashref merge_attributes );
  3         12  
9 3     3   1801 use Class::Usul::Types qw( ArrayRef Bool HashRef Logger SimpleStr Str );
  3         12  
  3         38  
10 3     3   6445 use File::DataClass::Types qw( Directory Path );
  3         129536  
  3         81  
11 3     3   5501 use File::Gettext;
  3         235979  
  3         154  
12 3     3   32 use File::Gettext::Constants qw( CONTEXT_SEP LOCALE_DIRS );
  3         10  
  3         227  
13 3     3   24 use File::Spec::Functions qw( tmpdir );
  3         6  
  3         294  
14 3     3   23 use Try::Tiny;
  3         10  
  3         175  
15 3     3   22 use Unexpected::Functions qw( inflate_placeholders );
  3         9  
  3         31  
16 3     3   1182 use Moo;
  3         9  
  3         20  
17              
18             # Public attributes
19 0     0   0 has 'l10n_attributes' => is => 'lazy', isa => HashRef, builder => sub { {} };
20              
21             has 'locale' => is => 'lazy', isa => SimpleStr, default => LANG;
22              
23             has 'localedir' => is => 'lazy', isa => Path, coerce => TRUE,
24 0     0   0 builder => sub { LOCALE_DIRS->[ 0 ] };
25              
26             has 'log' => is => 'ro', isa => Logger,
27 0     0   0 builder => sub { Class::Null->new };
28              
29             has 'tempdir' => is => 'lazy', isa => Directory, coerce => TRUE,
30 0     0   0 builder => sub { tmpdir };
31              
32             # Private attributes
33             has '_domains' => is => 'lazy', isa => ArrayRef[Str], builder => sub {
34 5   50 5   143 $_[ 0 ]->l10n_attributes->{domains} // [ 'messages' ] },
35             reader => 'domains';
36              
37             has '_source_name' => is => 'lazy', isa => SimpleStr, builder => sub {
38 5   50 5   186 $_[ 0 ]->l10n_attributes->{source_name} // 'po' },
39             reader => 'source_name';
40              
41             has '_use_country' => is => 'lazy', isa => Bool, builder => sub {
42 3   50 3   96 $_[ 0 ]->l10n_attributes->{use_country} // FALSE },
43             reader => 'use_country';
44              
45             # Class attributes
46             my $domain_cache = {}; my $locale_cache = {};
47              
48             # Private methods
49             my $_extract_lang_from = sub {
50             my ($self, $locale) = @_;
51              
52             exists $locale_cache->{ $locale } and return $locale_cache->{ $locale };
53              
54             my $sep = $self->use_country ? '.' : '_';
55             my $lang = (split m{ \Q$sep\E }msx, $locale.$sep )[ 0 ];
56              
57             return $locale_cache->{ $locale } = $lang;
58             };
59              
60             my $_load_domains = sub {
61             my ($self, $args) = @_; my $charset;
62              
63             assert $self, sub { $args->{locale} }, 'No locale id';
64              
65             my $locale = $args->{locale} or return;
66             my $lang = $self->$_extract_lang_from( $locale );
67             my $names = $args->{domains} // $args->{domain_names} // $self->domains;
68             my @names = grep { defined and length } @{ $names };
69             my $key = $lang.SEP.(join '+', @names );
70              
71             defined $domain_cache->{ $key } and return $domain_cache->{ $key };
72              
73             my $attrs = { %{ $self->l10n_attributes }, builder => $self,
74             source_name => $self->source_name, };
75              
76             defined $self->localedir and $attrs->{localedir} = $self->localedir;
77              
78             $locale =~ m{ \A (?: [a-z][a-z] )
79             (?: (?:_[A-Z][A-Z] )? \. ( [-_A-Za-z0-9]+ )? )?
80             (?: \@[-_A-Za-z0-9=;]+ )? \z }msx and $charset = $1;
81             $charset and $attrs->{charset} = $charset;
82              
83             my $domain = try { File::Gettext->new( $attrs )->load( $lang, @names ) }
84             catch { $self->log->error( $_ ); return };
85              
86             return $domain ? $domain_cache->{ $key } = $domain : undef;
87             };
88              
89             my $_gettext = sub {
90             my ($self, $key, $args) = @_;
91              
92             my $count = $args->{count} || 1;
93             my $default = $args->{no_default} ? NUL : $key;
94             my $domain = $self->$_load_domains( $args )
95             or return ($default, $args->{plural_key})[ $count > 1 ] // $default;
96             # Select either singular or plural translation
97             my ($nplurals, $plural) = (1, 0);
98              
99             if ($count > 1) { # Some languages have more than one plural form
100             ($nplurals, $plural) = $domain->{plural_func}->( $count );
101             defined $nplurals or $nplurals = 0;
102             defined $plural or $plural = 0;
103             $plural > $nplurals and $plural = $nplurals;
104             }
105              
106             my $id = defined $args->{context}
107             ? $args->{context}.CONTEXT_SEP.$key : $key;
108             my $msgs = $domain->{ $self->source_name } // {};
109             my $msg = $msgs->{ $id } // {};
110              
111             return @{ $msg->{msgstr} // [] }[ $plural ] // $default;
112             };
113              
114             # Construction
115             around 'BUILDARGS' => sub {
116             my ($orig, $class, @args) = @_; my $attr = $orig->( $class, @args );
117              
118             my $builder = $attr->{builder} or return $attr;
119             my $config = $builder->can( 'config' ) ? $builder->config : {};
120             my $keys = [ qw( l10n_attributes locale localedir tempdir ) ];
121              
122             merge_attributes $attr, $builder, [ 'log' ];
123             merge_attributes $attr, $config, $keys;
124              
125             return $attr;
126             };
127              
128             # Public methods
129             sub get_po_header {
130 1     1 1 1069 my ($self, $args) = @_;
131              
132 1 50 50     11 my $domain = $self->$_load_domains( $args // {} ) or return {};
133 1 50       7 my $header = $domain->{po_header} or return {};
134              
135 1   50     9 return $header->{msgstr} // {};
136             }
137              
138             sub invalidate_cache {
139 0     0 1 0 $domain_cache = {}; return;
  0         0  
140             }
141              
142             sub loc {
143 0     0 1 0 my $self = shift; return $self->localizer( $self->locale, @_ );
  0         0  
144             }
145              
146             sub localize {
147 18     18 1 10198 my ($self, $key, $args) = @_;
148              
149 18 50 50     77 defined $key or return; $key = "${key}"; chomp $key; $args //= {};
  18         50  
  18         54  
  18         67  
150              
151             # Lookup the message using the supplied key from the po file
152 18         72 my $text = $self->$_gettext( $key, $args );
153              
154 18 100 66     168 if (defined $args->{params} and ref $args->{params} eq 'ARRAY') {
155 12 100       99 0 > index $text, '[_' and return $text;
156              
157             # Expand positional parameters of the form [_<n>]
158             return inflate_placeholders [ '[?]', '[]', $args->{no_quote_bind_values}],
159 4         22 $text, @{ $args->{params} };
  4         32  
160             }
161              
162 6 50       53 0 > index $text, '{' and return $text;
163              
164             # Expand named parameters of the form {param_name}
165 0         0 my %args = %{ $args }; my $re = join '|', map { quotemeta $_ } keys %args;
  0         0  
  0         0  
  0         0  
166              
167 0 0       0 $text =~ s{ \{($re)\} }{ defined $args{ $1 } ? $args{ $1 } : "{${1}?}" }egmx;
  0         0  
168              
169 0         0 return $text;
170             }
171              
172             sub localizer {
173 7     7 1 3632 my ($self, $locale, $key, @args) = @_; my $car = $args[ 0 ];
  7         20  
174              
175 7 100       37 my $args = (is_hashref $car) ? { %{ $car } }
  1 100       7  
176             : { params => (is_arrayref $car) ? $car : [ @args ] };
177              
178 7   33     59 $args->{locale } //= $locale;
179 7   50     58 $args->{no_quote_bind_values} //= TRUE;
180              
181 7         29 return $self->localize( $key, $args );
182             }
183              
184             1;
185              
186             __END__
187              
188             =pod
189              
190             =head1 Name
191              
192             Class::Usul::L10N - Localise text strings
193              
194             =head1 Synopsis
195              
196             use Class::Usul::L10N;
197              
198             my $l10n = Class::Usul::L10N->new( {
199             localedir => 'path_to_message_catalogs',
200             log => Log::Handler->new, } );
201              
202             $local_text = $l10n->localize( 'message_to_localize', {
203             domains => [ 'message_file', 'another_message_file' ],
204             locale => 'de_DE',
205             params => { name => 'value', }, } );
206              
207             =head1 Description
208              
209             Localise text strings by looking them up in a GNU Gettext PO message catalogue
210              
211             =head1 Configuration and Environment
212              
213             A POSIX locale id has the form
214              
215             <language>_<country>.<charset>@<key>=<value>;...
216              
217             If the C<use_country> attribute is set to true in the constructor call
218             then the language and country are used from C<locale>. By default
219             C<use_country> is false and only the language from the C<locale>
220             attribute is used
221              
222             Defines the following attributes;
223              
224             =over 3
225              
226             =item C<l10n_attributes>
227              
228             Hash ref passed to the L<File::Gettext> constructor
229              
230             =over 3
231              
232             =item C<_domains>
233              
234             Names of the mo/po files to search for
235              
236             =item C<_source_name>
237              
238             Either C<po> for Portable Object (the default) or C<mo> for the Machine Object
239              
240             =item C<_use_country>
241              
242             See above
243              
244             =back
245              
246             =item C<localedir>
247              
248             Base directory to search for mo/po files
249              
250             =item C<log>
251              
252             Optional logging object
253              
254             =item C<tempdir>
255              
256             Directory to use for temporary files
257              
258             =back
259              
260             =head1 Subroutines/Methods
261              
262             =head2 BUILDARGS
263              
264             Monkey with the constructors signature
265              
266             =head2 BUILD
267              
268             Finish initialising the object
269              
270             =head2 get_po_header
271              
272             $po_header_hash_ref = $l10n->get_po_header( { locale => 'de' } );
273              
274             Returns a hash ref containing the keys and values of the PO header record
275              
276             =head2 invalidate_cache
277              
278             $l10n->invalidate_cache;
279              
280             Causes a reload of the domain files the next time a message is localised
281              
282             =head2 loc
283              
284             $local_text = $l10n->loc( $key, @args );
285              
286             Calls L</localizer> supplying L</locale> as the first argument
287              
288             =head2 localize
289              
290             $local_text = $l10n->localize( $key, $args );
291              
292             Localises the message indexed by C<$key>. The message catalogue is
293             loaded from a GNU Gettext portable object file. Returns the C<$key> if
294             the message is not in the catalogue (and C<< $args->{no_default} >> is
295             not true). Language is selected by the C<< $args->{locale} >>
296             attribute. Expands positional parameters of the form C<< [_<n>] >> if
297             C<< $args->{params} >> is an array reference of values to
298             substitute. Otherwise expands named attributes of the form
299             C<< {attr_name} >> using the C<$args> hash for substitution values. If
300             C<< $args->{quote_bind_values} >> is true the placeholder values are
301             displayed wrapped in quotes. The attribute C<< $args->{count} >> is
302             passed to the portable object files plural function which is used to
303             select either the singular or plural form of the translation. If
304             C<< $args->{context} >> is supplied it is prepended to the C<$key> before
305             the lookup in the catalogue takes place
306              
307             =head2 localizer
308              
309             $local_text = $l10n->localizer( $locale, $key, @args );
310              
311             Curries the call to L<localize>. It constructs the C<$args> parameter in the
312             call to L<localize> from the C<@args> parameter, defaulting the C<locale>
313             attribute to C<$locale>. The C<@args> parameter can be a hash reference,
314             an array reference or a list of values
315              
316             =head1 Diagnostics
317              
318             Asserts that the I<locale> attribute is set
319              
320             =head1 Dependencies
321              
322             =over 3
323              
324             =item L<Class::Usul::Constants>
325              
326             =item L<Class::Usul::Functions>
327              
328             =item L<File::DataClass::Types>
329              
330             =item L<File::Gettext>
331              
332             =item L<File::Gettext::Constants>
333              
334             =item L<Moo>
335              
336             =item L<Try::Tiny>
337              
338             =back
339              
340             =head1 Incompatibilities
341              
342             There are no known incompatibilities in this module
343              
344             =head1 Bugs and Limitations
345              
346             There are no known bugs in this module.
347             Please report problems to the address below.
348             Patches are welcome
349              
350             =head1 Author
351              
352             Peter Flanigan, C<< <pjfl@cpan.org> >>
353              
354             =head1 Acknowledgements
355              
356             Larry Wall - For the Perl programming language
357              
358             =head1 License and Copyright
359              
360             Copyright (c) 2017 Peter Flanigan. All rights reserved
361              
362             This program is free software; you can redistribute it and/or modify it
363             under the same terms as Perl itself. See L<perlartistic>
364              
365             This program is distributed in the hope that it will be useful,
366             but WITHOUT WARRANTY; without even the implied warranty of
367             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE
368              
369             =cut
370              
371             # Local Variables:
372             # mode: perl
373             # tab-width: 3
374             # End: