File Coverage

blib/lib/Locale/Wolowitz.pm
Criterion Covered Total %
statement 74 75 98.6
branch 23 34 67.6
condition 8 14 57.1
subroutine 11 11 100.0
pod 5 5 100.0
total 121 139 87.0


line stmt bran cond sub pod time code
1             package Locale::Wolowitz;
2              
3             # ABSTRACT: Dead simple localization with JSON.
4              
5 6     6   82855 use warnings;
  6         10  
  6         174  
6 6     6   20 use strict;
  6         6  
  6         92  
7 6     6   515 use utf8;
  6         16  
  6         22  
8              
9 6     6   97 use Carp;
  6         7  
  6         456  
10 6     6   2559 use JSON::MaybeXS qw/JSON/;
  6         36819  
  6         4878  
11              
12             our $VERSION = "1.004000";
13             $VERSION = eval $VERSION;
14              
15             =encoding utf-8
16              
17             =head1 NAME
18              
19             Locale::Wolowitz - Dead simple localization with JSON.
20              
21             =head1 SYNOPSIS
22              
23             # in ./i18n/locales.coll.json
24             {
25             "Welcome!": {
26             "he": "ברוכים הבאים!",
27             "es": "Bienvenido!"
28             },
29             "I'm using %1": {
30             "he": "אני משתמש ב%1",
31             "es": "Estoy usando %1"
32             },
33             "Linux": {
34             "he": "לינוקס"
35             }
36             }
37              
38             # in your app
39             use Locale::Wolowitz;
40              
41             my $w = Locale::Wolowitz->new( './i18n' );
42              
43             print $w->loc('Welcome!', 'es'); # prints 'Bienvenido!'
44              
45             print $w->loc("I'm using %1", 'he', $w->loc('Linux', 'he')); # prints "אני משתמש בלינוקס"
46              
47             # you can also directly load data (useful if data is not in files, but say in database)
48             $w->load_structure({
49             hello => {
50             he => 'שלום',
51             fr => 'bonjour'
52             }
53             });
54              
55             print $w->loc('hello', 'he'); # prints "שלום"
56              
57             =head1 DESCRIPTION
58              
59             Locale::Wolowitz is a very simple text localization system. Yes, another
60             localization system.
61              
62             Frankly, I never realized how to use the standard Perl localization systems
63             such as L, L, L or whatever.
64             It seems they are more meant to localize an application to the language
65             of the system on which its running, which isn't really what I need. Most of the
66             time, seeing as how I'm mostly writing web applications, I wish to localize
67             my applications/websites according to the user's wishes, not by the system.
68             For example, I may create a content management system where the user can
69             select the interface's language. Also, I grew to hate the standard .po
70             files, and thought using a JSON format might be more comfortable.
71              
72             Locale::Wolowitz allows you to provide different languages to end-users of your
73             applications. To some extent, when writing RESTful web applications, this means
74             you can perform language negotiation with visitors (see
75             L).
76              
77             Locale::Wolowitz works with JSON files. Each file can serve one or more languages.
78             When creating an instance of this module, you are required to pass a path
79             to a directory where your application's JSON localization files are present.
80             These are all loaded and merged into one big hash-ref (unless you tell the module
81             to only load a specific file), which is stored in memory. A file with only one
82             language has to be named .json (where is the name of the language,
83             you'd probably want to use the two-letter ISO 639-1 code). A file with multiple
84             languages must end with .coll.json (this requirement will probably be lifted in
85             the future).
86              
87             The basic idea is to write your application in a base language, and use
88             the JSON files to translate text to other languages. For example, lets say
89             you're writing your application in English and translating it to Hebrew,
90             Spanish, and Dutch. You put Spanish and Dutch translations in one file,
91             and since everybody hates Israel, you put Hebrew translations alone.
92             The Spanish and Dutch file can look like this:
93              
94             # es_and_nl.coll.json
95             {
96             "Welcome!": {
97             "es": "Bienvenido!",
98             "nl": "Welkom!"
99             },
100             "I'm using %1": {
101             "es": "Estoy usando %1",
102             "nl": "Ik gebruik %1"
103             },
104             "Linux": {} // this line can also be missing entirely
105             }
106              
107             While the Hebrew file can look like this:
108              
109             # he.json
110             {
111             "Welcome!": "ברוכים הבאים!",
112             "I'm using %1": "אני משתמש ב%1",
113             "Linux": "לינוקס"
114             }
115              
116             When loading these files, Locale::Wolowitz internally merges the two files into
117             one structure:
118              
119             {
120             "Welcome!" => {
121             "es" => "Bienvenido!",
122             "nl" => "Welkom!",
123             "he" => "ברוכים הבאים!",
124             },
125             "I'm using %1" => {
126             "es" => "Estoy usando %1",
127             "nl" => "Ik gebruik %1",
128             "he" => "אני משתמש ב%1",
129             },
130             "Linux" => {
131             "he" => "לינוקס",
132             }
133             }
134              
135             Notice the "%1" substrings above. This is a placeholder, just like in other
136             localization paradigms - they are replaced with content you provide, usually
137             dynamic content. In Locale::Wolowitz, placeholders are written with a percent
138             sign, followed by an integer, starting from 1 (e.g. %1, %2, %3). When passing
139             data for the placeholders, make sure you're passing scalars, or printable
140             objects, otherwise you'll encounter errors.
141              
142             We can also see here that Spanish and Dutch have no translation for "Linux".
143             Since Linux is written "Linux" in these languages, they have no translation.
144             When attempting to translate a string that has no translation to the requested
145             language, or has no reference in the JSON files at all, the string is
146             simply returned as is (but placeholders will still be replaced as expected).
147              
148             Say you write your application in English (and thus 'en' is your base
149             language). Since Locale::Wolowitz doesn't really know what your base language is,
150             you can translate texts within the same language. This is more useful when
151             you want to give some of your strings an identifier. For example:
152              
153             "copyrights": {
154             "en": "Copyrights, 2010 Ido Perlmuter",
155             "he": "כל הזכויות שמורות, 2010 עידו פרלמוטר"
156             }
157              
158             =head1 CONSTRUCTOR
159              
160             =head2 new( [ $path / $filename, \%options ] )
161              
162             Creates a new instance of this module. A path to a directory in
163             which JSON localization files exist, or a path to a specific localization
164             file, I be supplied. If you pass a directory, all JSON localization files
165             in it will be loaded and merged as described above. If you pass one file,
166             only that file will be loaded.
167              
168             Note that C will ignore dotfiles in the provided path (e.g.
169             hidden files, backups files, etc.).
170              
171             A hash-ref of options can also be provided. The only option currently supported
172             is C, which is on by default. If on, all JSON files are assumed to be in
173             UTF-8 character set and will be automatically decoded. Provide a false value
174             if your files are not UTF-8 encoded, for example:
175              
176             Locale::Wolowitz->new( '/path/to/files', { utf8 => 0 } );
177              
178             =cut
179              
180             sub new {
181 5     5 1 212558 my ($class, $path, $options) = @_;
182              
183 5   50     38 $options ||= {};
184             $options->{utf8} = 1
185 5 50       27 unless exists $options->{utf8};
186              
187 5         12 my $self = bless {}, $class;
188              
189 5         27 $self->{json} = JSON->new->relaxed;
190             $self->{json}->utf8
191 5 50       138 if $options->{utf8};
192              
193 5 100       20 $self->load_path($path)
194             if $path;
195              
196 5         18 return $self;
197             }
198              
199             =head1 OBJECT METHODS
200              
201             =head2 load_path( $path / $filename )
202              
203             Receives a path to a directory in which JSON localization files exist, or a
204             path to a specific localization file, and loads (and merges) the localization
205             data from the file(s). If localization data was already loaded previously,
206             the structure will be merged, with the new data taking precedence.
207              
208             You can call this method and L
209             as much as you want, the data from each call will be merged with existing data.
210              
211             =cut
212              
213             sub load_path {
214 3     3 1 6 my ($self, $path) = @_;
215              
216 3 50       27 croak "You must provide a path to localization directory."
217             unless $path;
218              
219 3   100     16 $self->{locales} ||= {};
220              
221 3         2 my @files;
222              
223 3 100       70 if (-d $path) {
    50          
224             # open the locales directory
225 1 50       27 opendir(PATH, $path)
226             || croak "Can't open localization directory: $!";
227            
228             # get all JSON files
229 1         16 @files = grep {/^[^.].*\.json$/} readdir PATH;
  4         13  
230              
231 1 50       9 closedir PATH
232             || carp "Can't close localization directory: $!";
233             } elsif (-e $path) {
234 2         16 my ($file) = ($path =~ m{/([^/]+)$})[0];
235 2         5 $path = $`;
236 2         5 @files = ($file);
237             } else {
238 0         0 croak "Path must be to a directory or a JSON file.";
239             }
240              
241             # load the files
242 3         7 foreach (@files) {
243             # read the file's contents and parse it as json
244 4 50       102 open(FILE, "$path/$_")
245             || croak "Can't open localization file $_: $!";
246 4         12 local $/;
247 4         68 my $json = ;
248 4 50       24 close FILE
249             || carp "Can't close localization file $_: $!";
250              
251 4         61 my $data = $self->{json}->decode($json);
252              
253             # is this a one-lang file or a collection?
254 4 100       26 if (m/\.coll\.json$/) {
    50          
255             # this is a collection of languages
256 2         7 foreach my $str (keys %$data) {
257 8         6 foreach my $lang (keys %{$data->{$str}}) {
  8         19  
258 14         30 $self->{locales}->{$str}->{$lang} = $data->{$str}->{$lang};
259             }
260             }
261             } elsif (m/\.json$/) { # has to be true
262 2         4 my $lang = $`;
263 2         7 foreach my $str (keys %$data) {
264 6         19 $self->{locales}->{$str}->{$lang} = $data->{$str};
265             }
266             }
267             }
268              
269 3         5 return 1;
270             }
271              
272             =head2 load_structure( \%structure, [ $lang ] )
273              
274             Receives a hash-ref of localization data similar to that in the JSON files
275             and loads it into the object (possibly merging with existing data, if any).
276             If C<$lang> is supplied, a one-to-one structure will be assumed, like so:
277              
278             load_structure(
279             { "hello" => "שלום", "world" => "עולם" },
280             'he'
281             )
282              
283             Or, if C<$lang> is not provided, the structure must be the multiple language
284             structure, like so:
285              
286             load_structure({
287             "hello" => {
288             "he" => "שלום",
289             "fr" => "bonjour"
290             },
291             "world" => {
292             "he" => "עולם",
293             "fr" => "monde",
294             "it" => "mondo"
295             }
296             })
297              
298             You can call this method and L
299             as much as you want, the data from each call will be merged with existing data.
300              
301             =cut
302              
303             sub load_structure {
304 3     3 1 733 my ($self, $struct) = @_;
305              
306 3 50 33     24 croak "The structure to load must be a hash-ref"
307             unless $struct && ref $struct eq 'HASH';
308              
309 3   50     22 $self->{locales} ||= {};
310              
311 3         12 foreach (keys %$struct) {
312 6   50     26 $self->{locales}->{$_} ||= {};
313 6         6 foreach my $lang (keys %{$struct->{$_}}) {
  6         16  
314 9         21 $self->{locales}->{$_}->{$lang} = $struct->{$_}->{$lang};
315             }
316             }
317              
318 3         7 return 1;
319             }
320              
321             =head2 loc( $msg, $lang, [ @args ] )
322              
323             Returns the string C<$msg>, translated to the requested language (if such
324             a translation exists, otherwise no traslation occurs). Any other parameters
325             passed to the method (C<@args>) are injected to the placeholders in the string
326             (if present).
327              
328             If an argument is an array ref, it'll be replaced with
329             a recursive call to C with its elements, with the C<$lang>
330             argument automatically added. In other
331             words, the following two statements are equivalent:
332              
333             print $w->loc("I'm using %1", 'he', $w->loc('Linux', 'he'));
334             # same result as
335             print $w->loc("I'm using %1", 'he', [ 'Linux' ]);
336              
337              
338             =cut
339              
340             sub loc {
341 29     29 1 1148 my ($self, $msg, $lang, @args) = @_;
342              
343 29 100       58 return unless defined $msg; # undef strings are passed back as-is
344 28 50       59 return $msg unless $lang;
345              
346             @args = map {
347 28 100       39 ref $_ ne 'ARRAY' ? $_ : do {
  40         101  
348 1         2 my @args = @$_;
349 1         2 splice @args, 1, 0, $lang;
350 1         4 $self->loc( @args );
351             }
352             } @args;
353              
354 28 100 66     132 my $ret = $self->{locales}->{$msg} && $self->{locales}->{$msg}->{$lang} ? $self->{locales}->{$msg}->{$lang} : $msg;
355              
356 28         139 $ret =~ s/%(\d+)/$args[$1-1]/g;
357              
358 28         113 return $ret;
359             }
360              
361             =head2 loc_for( $lang )
362              
363             Returns a function ref that is like C, but with the C<$lang> curried away.
364              
365             use Locale::Wolowitz;
366              
367             my $w = Locale::Wolowitz->new( './i18n' );
368              
369             my $french_loc = $w->loc_for('fr');
370             my $german_loc = $w->loc_for('de');
371              
372             print $french_loc->('Welcome!'); # equivalent to $w->loc( 'Welcome!', 'fr' )
373              
374             =cut
375              
376             sub loc_for {
377 1     1 1 5 my( $self, $lang ) = @_;
378              
379             return sub {
380 2     2   5 my $text = shift;
381 2         6 $self->loc( $text, $lang, @_ );
382 1         5 };
383             }
384              
385              
386             =head1 DIAGNOSTICS
387              
388             The following exceptions are thrown by this module:
389              
390             =over
391              
392             =item C<< "You must provide a path to localization directory." >>
393              
394             This exception is thrown if you haven't provided the C subroutine
395             a path to a localization file, or a directory of localization files. Read
396             the documentation for the C subroutine above.
397              
398             =item C<< "Can't open localization directory: %s" and "Can't close localization directory: %s" >>
399              
400             This exception is thrown if Locale::Wolowitz failed to open/close the directory
401             of the localization files. This will probably happen due to permission
402             problems. The error message should include the actual reason for the failure.
403              
404             =item C<< "Path must be to a directory or a JSON file." >>
405              
406             This exception is thrown if you passed a wrong value to the C subroutine
407             as the path to the localization directory/file. Either the path is wrong and thus
408             does not exist, or the path does exist, but is not a directory and not a file.
409              
410             =item C<< "Can't open localization file %s: %s" and "Can't close localization file %s: %s" >>
411              
412             This exception is thrown if Locale::Wolowitz fails to open/close a specific localization
413             file. This will usually happen because of permission problems. The error message
414             will include both the name of the file, and the actual reason for the failure.
415              
416             =back
417              
418             =head1 CONFIGURATION AND ENVIRONMENT
419              
420             C requires no configuration files or environment variables.
421              
422             =head1 DEPENDENCIES
423              
424             C B on the following CPAN modules:
425              
426             =over
427              
428             =item * L
429              
430             =item * L
431              
432             =back
433              
434             C recommends L or L for faster
435             parsing of JSON files.
436              
437             =head1 INCOMPATIBILITIES WITH OTHER MODULES
438              
439             None reported.
440              
441             =head1 BUGS AND LIMITATIONS
442              
443             No bugs have been reported.
444              
445             Please report any bugs or feature requests to
446             C, or through the web interface at
447             L.
448              
449             =head1 AUTHOR
450              
451             Ido Perlmuter
452              
453             =head1 LICENSE AND COPYRIGHT
454              
455             Copyright (c) 2010-2017, Ido Perlmuter C<< ido@ido50.net >>.
456              
457             This module is free software; you can redistribute it and/or
458             modify it under the same terms as Perl itself, either version
459             5.8.1 or any later version. See L
460             and L.
461              
462             The full text of the license can be found in the
463             LICENSE file included with this module.
464              
465             =head1 DISCLAIMER OF WARRANTY
466              
467             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
468             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
469             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
470             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
471             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
472             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
473             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
474             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
475             NECESSARY SERVICING, REPAIR, OR CORRECTION.
476              
477             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
478             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
479             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
480             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
481             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
482             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
483             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
484             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
485             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
486             SUCH DAMAGES.
487              
488             =cut
489              
490             1;
491             __END__