File Coverage

blib/lib/Lingua/Text.pm
Criterion Covered Total %
statement 92 98 93.8
branch 39 52 75.0
condition 14 18 77.7
subroutine 16 16 100.0
pod 4 4 100.0
total 165 188 87.7


line stmt bran cond sub pod time code
1             package Lingua::Text;
2              
3 5     5   670013 use strict;
  5         9  
  5         147  
4 5     5   18 use warnings;
  5         6  
  5         181  
5              
6 5     5   18 use Carp;
  5         7  
  5         227  
7 5     5   2212 use HTML::Entities;
  5         23224  
  5         365  
8 5     5   1961 use Params::Get;
  5         44981  
  5         227  
9 5     5   30 use Scalar::Util;
  5         6  
  5         115  
10 5     5   2042 use I18N::LangTags::Detect;
  5         21260  
  5         373  
11              
12             # TODO: Investigate Locale::Maketext
13              
14             =head1 NAME
15              
16             Lingua::Text - Class to contain text in many different languages
17              
18             =head1 VERSION
19              
20             Version 0.08
21              
22             =cut
23              
24             our $VERSION = '0.08';
25              
26             use overload (
27             # '==' => \&equal,
28             # '!=' => \¬_equal,
29             '""' => \&as_string,
30 62     62   137 bool => sub { 1 },
31 5         46 fallback => 1 # So that boolean tests don't cause as_string to be called
32 5     5   35 );
  5         14  
33              
34             =head1 SYNOPSIS
35              
36             Hold many texts in one object,
37             thereby encapsulating internationalized text.
38              
39             use Lingua::Text;
40              
41             my $str = Lingua::Text->new();
42              
43             $str->fr('Bonjour Tout le Monde');
44             $str->en('Hello, World');
45              
46             $ENV{'LANG'} = 'en_GB';
47             print "$str\n"; # Prints Hello, World
48             $ENV{'LANG'} = 'fr_FR';
49             print "$str\n"; # Prints Bonjour Tout le Monde
50             $ENV{'LANG'} = 'de_DE';
51             print "$str\n"; # Prints nothing
52              
53             my $text = Lingua::Text->new('hello'); # Initialises the 'current' language
54              
55             =cut
56              
57             =head1 METHODS
58              
59             =head2 new
60              
61             Create a Lingua::Text object.
62              
63             use Lingua::Text;
64              
65             my $str = Lingua::Text->new({ 'en' => 'Here', 'fr' => 'Ici' });
66              
67             Accepts various input formats, e.g., HASH or reference to a HASH.
68             Clones existing objects with or without modifications.
69             Uses Carp::carp to log warnings for incorrect usage or potential mistakes.
70              
71             =cut
72              
73             sub new {
74 15     15 1 610515 my $class = shift;
75              
76             # Handle hash or hashref arguments
77 15         28 my %args;
78 15 100 100     123 if((scalar(@_) == 1) && (!ref($_[0])) && (my $lang = _get_language())) {
    100 66        
79 2         5 $args{$lang} = $_[0];
80             } elsif(my $params = Params::Get::get_params(undef, @_)) {
81 6         139 %args = %{$params};
  6         30  
82             }
83              
84 15 100       758 if(!defined($class)) {
    100          
85 1 50       12 if((scalar keys %args) > 0) {
86             # Using Lingua::Text->new(), not Lingua::Text::new()
87 0         0 carp(__PACKAGE__, ' use ->new() not ::new() to instantiate');
88 0         0 return;
89             }
90              
91             # FIXME: this only works when no arguments are given
92 1         3 $class = __PACKAGE__;
93             } elsif(Scalar::Util::blessed($class)) {
94             # If $class is an object, clone it with new arguments
95 2 100       8 if(scalar(%args)) {
96 1         2 return bless { texts => {%{$class->{'texts'}}, %args} }, ref($class);
  1         7  
97             }
98 1         4 return bless { %{$class} }, ref($class);
  1         10  
99             }
100              
101             # Return the blessed object
102 13 100       33 if(scalar(%args)) {
103 7         30 return bless { texts => \%args }, $class;
104             }
105              
106 6         39 return bless { }, $class;
107             }
108              
109             =head2 set
110              
111             Sets a text in a language.
112              
113             $str->set({ text => 'House', lang => 'en' });
114              
115             Autoload will do this for you as
116              
117             $str->en('House');
118              
119             =cut
120              
121             sub set
122             {
123 9     9 1 4497 my $self = shift;
124 9         37 my $params = Params::Get::get_params('text', @_);
125              
126 8   100     242 my $lang = $params->{'lang'} || $self->_get_language();
127 8 100       21 if(!defined($lang)) {
128 1         4 Carp::carp(__PACKAGE__, ': usage: set(text => text, lang => $language)');
129 1         362 return;
130             }
131              
132 7         13 my $text = $params->{'text'};
133 7 100       18 if(!defined($text)) {
134 2         7 Carp::carp(__PACKAGE__, ': usage: set(text => text, lang => $language)');
135 2         835 return;
136             }
137              
138 5         10 $self->{'texts'}->{$lang} = $text;
139              
140 5         17 return $self;
141             }
142              
143             # https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html
144             # https://www.gnu.org/software/gettext/manual/html_node/The-LANGUAGE-variable.html
145             sub _get_language
146             {
147 23     23   78 for my $tag (I18N::LangTags::Detect::detect()) {
148 21 50       2601 if ($tag =~ /^([a-z]{2})/i) {
149 21         142 return lc($1);
150             }
151             }
152 2 50 66     389 if(($ENV{'LANGUAGE'}) && ($ENV{'LANGUAGE'} =~ /^([a-z]{2})/i)) {
153 0         0 return lc($1);
154             }
155              
156 2         4 foreach my $variable('LC_ALL', 'LC_MESSAGES', 'LANG') {
157 6         15 my $val = $ENV{$variable};
158 6 50       15 next unless(defined($val));
159              
160 0 0       0 if($val =~ /^([a-z]{2})/i) {
161 0         0 $val = lc($1);
162 0 0       0 return lc($val) if _is_valid_language($val);
163             }
164             }
165              
166             # if(defined($ENV{'LANG'}) && (($ENV{'LANG'} =~ /^C\./) || ($ENV{'LANG'} eq 'C'))) {
167             # return 'en';
168             # }
169 2 50 33     8 return 'en' if defined $ENV{'LANG'} && $ENV{'LANG'} =~ /^C(\.|$)/;
170 2         10 return; # undef
171             }
172              
173             =head2 as_string
174              
175             Returns the text in the language requested in the parameter.
176             If that parameter is not given, the system language is used.
177              
178             my $text = Lingua::Text->new(en => 'boat', fr => 'bateau');
179             print $text->as_string(), "\n";
180             print $text->as_string('fr'), "\n";
181             print $text->as_string({ lang => 'en' }), "\n";
182              
183             =cut
184              
185             sub as_string {
186 24     24 1 2938 my $self = shift;
187 24         36 my %params;
188              
189 24 100       96 if(ref($_[0]) eq 'HASH') {
    100          
190 3         5 %params = %{$_[0]};
  3         10  
191             } elsif((scalar(@_) % 2) == 0) {
192 19 100       51 if(defined($_[0])) {
193 1         5 %params = @_;
194             }
195             } else {
196 2         6 $params{'lang'} = shift;
197             }
198              
199 24 100 100     89 if(my $lang = ($params{'lang'} || $self->_get_language())) {
200 23         139 return $self->{'texts'}->{$lang};
201             }
202 1         4 Carp::carp(__PACKAGE__, ': usage: as_string(lang => $language)');
203             }
204              
205             =head2 encode
206              
207             =encoding utf-8
208              
209             Turns the encapsulated texts into HTML entities
210              
211             my $text = Lingua::Text->new(en => 'study', fr => 'étude')->encode();
212             print $text->fr(), "\n"; # Prints étude
213              
214             =cut
215              
216             sub encode {
217 3     3 1 597 my $self = shift;
218              
219 3         5 while(my($k, $v) = each(%{$self->{'texts'}})) {
  9         167  
220 6 100       37 utf8::decode($v) unless utf8::is_utf8($v); # Only decode if not already UTF-8
221 6         17 $self->{'texts'}->{$k} = HTML::Entities::encode_entities($v);
222             }
223 3         15 return $self;
224             }
225              
226             sub AUTOLOAD
227             {
228 31     31   5115 our $AUTOLOAD;
229 31 50       252 my $self = shift or return;
230              
231             # Extract the key name from the AUTOLOAD variable
232 31         190 my ($key) = $AUTOLOAD =~ /::(\w+)$/;
233              
234             # Skip if called on destruction
235 31 100       534 return if $key eq 'DESTROY';
236              
237             # Ensure the key is called on the correct package object
238 16 50       35 return unless ref($self) eq __PACKAGE__;
239              
240             # Only allow 2-letter language codes
241 16 50       47 return unless $key =~ /^[a-z]{2}$/i;
242              
243 16 50       28 return unless _is_valid_language($key);
244              
245 16 100       49 if(my $value = shift) {
246             # Set the requested language ($key) to the given text ($value)
247 5         14 $self->{'texts'}->{$key} = $value;
248             }
249              
250             # Get the requested language ($key)
251 16         79 return $self->{'texts'}->{$key};
252             }
253              
254             # Language validation
255             sub _is_valid_language {
256 16     16   42 my $lang = shift;
257 16         77 return $lang =~ /^[a-z]{2}(?:_[A-Z]{2})?$/; # en or en_US format
258             }
259              
260             =head1 AUTHOR
261              
262             Nigel Horne, C<< >>
263              
264             =head1 BUGS
265              
266             There's no decode() (yet),
267             so you'll have to be extra careful to avoid double encoding.
268              
269             =head1 SEE ALSO
270              
271             =head1 SUPPORT
272              
273             This module is provided as-is without any warranty.
274              
275             You can find documentation for this module with the perldoc command.
276              
277             perldoc Lingua::Text
278              
279             You can also look for information at:
280              
281             =over 4
282              
283             =item * MetaCPAN
284              
285             L
286              
287             =item * RT: CPAN's request tracker
288              
289             L
290              
291             =item * CPANTS
292              
293             L
294              
295             =item * CPAN Testers' Matrix
296              
297             L
298              
299             =item * CPAN Testers Dependencies
300              
301             L
302              
303             =back
304              
305             =head1 LICENCE AND COPYRIGHT
306              
307             Copyright 2021-2026 Nigel Horne.
308              
309             This program is released under the following licence: GPL2 for personal use on
310             a single computer.
311             All other users (for example, Commercial, Charity, Educational, Government)
312             must apply in writing for a licence for use from Nigel Horne at ``.
313              
314             =cut
315              
316             1;