File Coverage

lib/Text/PO/Gettext.pm
Criterion Covered Total %
statement 358 403 88.8
branch 79 146 54.1
condition 46 114 40.3
subroutine 78 83 93.9
pod 51 51 100.0
total 612 797 76.7


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## PO Files Manipulation - ~/lib/Text/PO/Gettext.pm
3             ## Version v0.3.2
4             ## Copyright(c) 2023 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2021/07/12
7             ## Modified 2025/11/29
8             ## All rights reserved
9             ##
10             ## This program is free software; you can redistribute it and/or modify it
11             ## under the same terms as Perl itself.
12             ##----------------------------------------------------------------------------
13             package Text::PO::Gettext;
14             BEGIN
15             {
16 2     2   503434 use strict;
  2         5  
  2         87  
17 2     2   11 use warnings;
  2         4  
  2         142  
18 2     2   2782 warnings::register_categories( 'Text::PO' );
19 2     2   31 use parent qw( Module::Generic );
  2         22  
  2         16  
20 2     2   184 use vars qw( $VERSION $L10N $DOMAIN_RE $LOCALE_RE );
  2         5  
  2         210  
21 2     2   1077 use I18N::Langinfo qw( langinfo );
  2         1162  
  2         222  
22 2     2   15 use Module::Generic::Global ':const';
  2         4  
  2         446  
23 2     2   17 use POSIX ();
  2         5  
  2         51  
24 2     2   2283 use Text::PO;
  2         7  
  2         21  
25             # l10n_id => lang => string => local string
26             # our $L10N = {};
27             # Since version v0.3.2, we now use Module::Generic::Global for thread safety
28 2         13 our $DOMAIN_RE = qr/^[a-z]+(\.[a-zA-Z0-9\_\-]+)*$/;
29             # Taken from Unicode BCP47 via Locale::Unicode
30 2         8 our $LOCALE_RE = qr/
31             (?<locale>
32             (?<locale_lang>
33             (?:
34             # "root" is treated as a special Unicode language subtag in the LDML and required in the valid pattern
35             (?<root>root)
36             |
37             (?<language>[a-z]{2})
38             |
39             (?<language3>[a-z]{3})
40             )
41             # "Up to three optional extended language subtags composed of three letters each, separated by hyphens"
42             # "There is currently no extended language subtag registered in the Language Subtag Registry without an equivalent and preferred primary language subtag"
43             (?:
44             [_-]
45             (?<extended>
46             [a-z]{3}
47             (?:\-[a-z]{3}){0,2}
48             )
49             )?
50             )
51             # ISO 15924 for scripts
52             (?:
53             [_-]
54             (?:
55             (?<script>[A-Z][a-z]{3})
56             |
57             (?<script>[a-z]{4})
58             )
59             )?
60             (?:
61             (?:
62             [_-]
63             (?<territory>
64             (?<country_code>[A-Z]{2})
65             |
66             # BCP47, section 2.2.4.4: the UN Standard Country or Area Codes for Statistical Use
67             # We are careful not to catch a following variant starting with a digit.
68             (?<region>\d{3}(?!\d))
69             )
70             )?
71             # "Optional variant subtags, separated by hyphens, each composed of five to eight letters, or of four characters starting with a digit"
72             # ca-ES-valencia
73             # country code can be skipped if the variant is limited to a country
74             # be-tarask
75             (?:
76             [_-]
77             (?<variant>
78             (?:
79             (?:[[:alnum:]]{5,8})
80             |
81             (?:\d[[:alnum:]]{3})
82             )
83             (?:
84             [_-]
85             (?:
86             (?:[[:alnum:]]{5,8})
87             |
88             (?:\d[[:alnum:]]{3})
89             )
90             )*
91             )
92             )?
93             )?
94             (?:\.(?<locale_encoding>[\w-]+))?
95             )
96             /xi;
97 2         64 our $VERSION = 'v0.3.2';
98             };
99              
100 2     2   16 use strict;
  2         5  
  2         62  
101 2     2   11 use warnings;
  2         3  
  2         1962  
102              
103             sub init
104             {
105 2     2 1 759319 my $self = shift( @_ );
106 2         102 $self->{category} = 'LC_MESSAGES';
107 2         12 $self->{domain} = undef;
108             # We also try LANGUAGE because GNU gettext actually only recognise LANGUAGE
109             # For example: LANGUAGE=fr_FR.utf-8 TEXTDOMAINDIR=./t gettext -d "com.example.api" -s "Bad Request"
110 2   33     35 $self->{locale} = $ENV{LANG} || $ENV{LANGUAGE};
111 2         10 $self->{path} = undef;
112 2         12 $self->{plural} = [];
113 2         15 $self->{use_json} = 1;
114 2         15 $self->{_init_strict_use_sub} = 1;
115 2         37 $self->{_init_params_order} = [qw( category path domain locale plural use_json )];
116 2 50       31 $self->SUPER::init( @_ ) || return( $self->pass_error );
117 2 50 33     3180 if( !defined( $self->{path} ) || !length( $self->{path} ) )
118             {
119 0         0 return( $self->error( "No directory path was provided for localisation" ) );
120             }
121 2 50       83 $self->textdomain( $self->{domain} ) || return( $self->pass_error );
122 2         58450 return( $self );
123             }
124              
125             sub addItem
126             {
127 0     0 1 0 my $self = shift( @_ );
128 0         0 my( $locale, $key, $value ) = @_;
129 0         0 my $hash = $self->getDomainHash();
130 0 0 0     0 return( $self->error( "No locale was provided." ) ) if( !defined( $locale ) || !length( $locale ) );
131 0 0 0     0 return( $self->error( "No msgid was provided." ) ) if( !defined( $key ) || !length( $key ) );
132 0         0 $locale = $self->locale_unix( $locale );
133 0 0       0 if( !$self->isSupportedLanguage( $locale ) )
134             {
135 0         0 return( $self->error( "Language requested \"${locale}\" to add item is not supported." ) );
136             }
137 0         0 $hash->{ $locale }->{ $key } = { msgid => $key, msgstr => $value };
138 0         0 return( $hash->{ $locale }->{ $key } );
139             }
140              
141 12     12 1 6633 sub category { return( shift->_set_get_scalar_as_object( 'category', @_ ) ); }
142              
143 1     1 1 1048 sub charset { return( shift->_get_po->charset ); }
144              
145 1     1 1 1731 sub contentEncoding { return( shift->_get_po->content_encoding ); }
146              
147 1     1 1 21 sub contentType { return( shift->_get_po->content_type ); }
148              
149 1     1 1 6 sub currentLang { return( shift->_get_po->current_lang ); }
150              
151 1     1 1 775 sub dgettext { return( shift->dngettext( @_ ) ); }
152              
153             sub dngettext
154             {
155 14     14 1 14830 my $self = shift( @_ );
156 14         69 my $opts = {};
157 14 100       111 $opts = pop( @_ ) if( ref( $_[-1] ) eq 'HASH' );
158 14         72 my( $domain, $msgid, $msgidPlural, $count ) = @_;
159 14         40 my $default;
160             my $index;
161 14 100 66     94 if( !defined( $count ) || $count !~ /^\d+$/ )
162             {
163 10   33     66 $default = $msgidPlural || $msgid;
164             }
165 14 100 100     107 if( !exists( $opts->{locale} ) || !length( $opts->{locale} ) )
166             {
167 7         29 $opts->{locale} = $self->locale;
168             }
169 14         6685 my $hash = $self->getDomainHash({ domain => $domain });
170 14         99 my $plural = $self->plural;
171 14 50       13562 if( !exists( $hash->{ $opts->{locale} } ) )
172             {
173 0 0       0 warn( "No locale \"$opts->{locale}\" found for the domain \"${domain}\".\n" ) if( $self->_is_warnings_enabled( 'Text::PO' ) );
174 0         0 return( Text::PO::String->new( $default ) );
175             }
176 14         107 my $l10n = $hash->{ $opts->{locale} };
177 14         78 my $dict = $l10n->{ $msgid };
178 14 50       68 if( $dict )
179             {
180 14 50       120 if( $plural->length == 0 )
181             {
182 0         0 $plural = $self->getPlural();
183             }
184 14 100       579894 if( ref( $dict->{msgstr} ) eq 'ARRAY' )
185             {
186 4 50 33     10585 if( $self->_is_number( $count ) &&
187             int( $plural->[0] ) > 0 )
188             {
189 2     2   18 no warnings 'once';
  2         4  
  2         13041  
190 4         264 my $n = $count;
191 4         17 my $expr = $plural->[1];
192 4         44 $expr =~ s/(?:^|\b)(?<!\$)(n)(?:\b|$)/$n/g;
193 4         328 $index = eval( $expr );
194 4 50       22 if( $@ )
195             {
196 0 0       0 warn( "Warning only: problem evaluating expression '$expr' for locale ", $opts->{locale}, ": $@" ) if( $self->_is_warnings_enabled( 'Text::PO' ) );
197             }
198             else
199             {
200 4         11 $index = int( $index );
201             }
202             }
203             else
204             {
205 0         0 $index = 0;
206             }
207             # return( join( '', @{$dict->{msgstr}->[ $index ]} ) || $default );
208 4 50       23 my $locale_str = ref( $dict->{msgstr}->[ $index ] ) eq 'ARRAY' ? join( '', @{$dict->{msgstr}->[ $index ]} ) : $dict->{msgstr}->[ $index ];
  4         17  
209 4 50       74 return( Text::PO::String->new( $locale_str => $opts->{locale} ) ) if( length( "$locale_str" ) );
210 0         0 return( Text::PO::String->new( $default ) );
211             }
212 10   33     26128 return( $dict->{msgstr} || $default );
213             }
214             else
215             {
216 0 0       0 warn( "No dictionary was found for msgid \"${msgid}\" and domain \"${domain}\"" ) if( $self->_is_warnings_enabled( 'Text::PO' ) );
217             }
218 0         0 return( $default );
219             }
220              
221             sub domain
222             {
223 40     40 1 8787 my $self = shift( @_ );
224 40 100       180 if( @_ )
225             {
226 2         5 my $v = shift( @_ );
227 2 50       123 if( !$v )
    50          
228             {
229 0         0 return( $self->error( "No domain was provided." ) );
230             }
231             elsif( $v !~ /^$DOMAIN_RE$/ )
232             {
233 0         0 return( $self->error( "Domain provided \"$v\" contains illegal characters." ) );
234             }
235 2         26 my $caller = [caller(1)]->[3];
236             # We do not call textdomain upon init, because we need both domain and locale to be set first
237             # textdomain() is called directly in init()
238 2 50       17 $self->textdomain( $v ) unless( $caller eq 'Module::Generic::init' );
239 2         10 $self->{domain} = $v;
240             }
241 40         215 return( $self->_set_get_scalar_as_object( 'domain' ) );
242             }
243              
244             sub exists
245             {
246 2     2 1 3236 my $self = shift( @_ );
247 2         38 my $lang = shift( @_ );
248 2 50       875 if( !defined( $lang ) )
    50          
    50          
249             {
250 0         0 return( $self->error( "No language to check for existence was provided." ) );
251             }
252             elsif( !length( $lang ) )
253             {
254 0         0 return( $self->error( "Language provided to check for existence is null." ) );
255             }
256             elsif( $lang !~ /^$LOCALE_RE$/ )
257             {
258 0         0 return( $self->error( "Unsupported locale format \"${lang}\"." ) );
259             }
260 2         20 $lang = $self->locale_unix( $lang );
261 2         22 my $hash = $self->getDomainHash();
262 2         35 return( exists( $hash->{ $lang } ) );
263             }
264              
265             sub fetchLocale
266             {
267 3     3 1 19 my $self = shift( @_ );
268 3         13 my $key = shift( @_ );
269 3         50 my $hash = $self->getDomainHash();
270 3         18 my $spans = [];
271             # Browsing through each available locale language
272             # Make it predictable using sort()
273 3         42 foreach my $k ( sort( keys( %$hash ) ) )
274             {
275 6         56 my $locWeb = $self->locale_web( $k );
276 6         45 push( @$spans, "<span lang=\"${locWeb}\">" . $self->dngettext( $self->domain, $key, { locale => $k }) . '</span>' );
277             }
278 3         83 return( $self->new_array( $spans ) );
279             }
280              
281 2     2 1 229490 sub getDataPath { return( $ENV{TEXTDOMAINDIR} ); }
282              
283             sub getDaysLong
284             {
285 2     2 1 103816 my $self = shift( @_ );
286 2         18 my $opts = $self->_get_args_as_hash( @_ );
287 2         1353 my $ref = $self->_get_days( $self->locale );
288 2         6 my $days = $ref->[1];
289 2 100       8 if( $opts->{monday_first} )
290             {
291             # Move Sunday at the end
292 1         4 push( @$days, shift( @$days ) );
293             }
294 2         14 return( $days );
295             }
296              
297             sub getDaysShort
298             {
299 2     2 1 99139 my $self = shift( @_ );
300 2         16 my $opts = $self->_get_args_as_hash( @_ );
301 2         1339 my $ref = $self->_get_days( $self->locale );
302 2         7 my $days = $ref->[0];
303 2 100       8 if( $opts->{monday_first} )
304             {
305             # Move Sunday at the end
306 1         5 push( @$days, shift( @$days ) );
307             }
308 2         13 return( $days );
309             }
310              
311             sub getDomainHash
312             {
313 38     38 1 29759 my $self = shift( @_ );
314 38         244 my $opts = $self->_get_args_as_hash( @_ );
315 38   66     39341 $opts->{domain} //= $self->domain;
316 38   33     24750 my $class = ref( $self ) || $self;
317              
318 38         203 my $repo = Module::Generic::Global->new( 'l10n' => $class );
319 38   50     5921 my $hash = $repo->get // {};
320 38 50       33436 if( !exists( $hash->{ $opts->{domain} } ) )
321             {
322 0         0 return( $self->error( "No locale data for domain \"$opts->{domain}\"." ) );
323             }
324 38         391 my $l10n = $hash->{ $opts->{domain} };
325 38 100 66     428 if( exists( $opts->{locale} ) &&
326             defined( $opts->{locale} ) )
327             {
328 18         130 $opts->{locale} = $self->locale_unix( $opts->{locale} );
329 18 50       90 if( length( $opts->{locale} ) == 0 )
330             {
331 0         0 return( $self->error( "Locale was provided, but is empty." ) );
332             }
333 18         183 return( $l10n->{ $opts->{locale} } );
334             }
335 20         159 return( $l10n );
336             }
337              
338 2     2 1 179463 sub getLangDataPath { return( $ENV{TEXTLOCALEDIR} ); }
339              
340             sub getLanguageDict
341             {
342 2     2 1 15391 my $self = shift( @_ );
343 2   50     21 my $lang = shift( @_ ) || return( $self->error( "Language provided, to get its dictionary, is undefined or null." ) );
344 2 50       882 if( $lang !~ /^$LOCALE_RE$/ )
345             {
346 0         0 return( $self->error( "Locale provided (${lang}) is in an unsupported format." ) );
347             }
348 2         32 $lang = $self->locale_unix( $lang );
349              
350 2 100       147 if( !$self->isSupportedLanguage( $lang ) )
351             {
352 1         42 return( $self->error( "Language provided (${lang}), to get its dictionary, is unsupported." ) );
353             }
354 1         15 my $hash = $self->getDomainHash();
355 1 50       6 if( !exists( $hash->{ $lang } ) )
356             {
357 0         0 return( $self->error( "Language provided (${lang}), to get its dictionary, could not be found. This is weird. Most likely a configuration mistake." ) );
358             }
359 1         42 return( $hash->{ $lang } );
360             }
361              
362 1     1 1 27778 sub getLocale { return( shift->locale ); }
363              
364             sub getLocales
365             {
366 1     1 1 2254 my $self = shift( @_ );
367 1   50     14 my $key = shift( @_ ) || return( $self->error( "No text provided to get its localised equivalent" ) );
368 1   50     15 my $res = $self->fetchLocale( $key ) || return( $self->pass_error );
369 1 50       802 if( scalar( @$res ) > 0 )
370             {
371 1         11 return( join( "\n", @$res ) );
372             }
373             else
374             {
375 0         0 return( $key );
376             }
377             }
378              
379             sub getLocalesf
380             {
381 1     1 1 1465 my $self = shift( @_ );
382 1   50     19 my $key = shift( @_ ) || return( $self->error( "No text provided to get its localised equivalent" ) );
383 1   50     6 my $res = $self->fetchLocale( $key ) || return( $self->pass_error );
384 1 50       815 if( scalar( @$res ) > 0 )
385             {
386 1         7 for( my $i = 0; $i < scalar( @$res ); $i++ )
387             {
388 2         16 $res->[$i] = sprintf( $res->[$i], @_ );
389             }
390 1         11 return( join( "\n", @$res ) );
391             }
392             else
393             {
394 0         0 return( sprintf( $key, @_ ) );
395             }
396             }
397              
398             sub getMetaKeys
399             {
400 1     1 1 1547 my $self = shift( @_ );
401 1         9 my $hash = $self->getDomainHash({ locale => $self->locale });
402 1   50     15 my $po = $hash->{_po} || return( $self->error( "Unable to get the po object in the locale data hash" ) );
403 1         22 return( $po->meta_keys );
404             }
405              
406             sub getMetaValue
407             {
408 1     1 1 46729 my $self = shift( @_ );
409 1   50     11 my $field = shift( @_ ) || return( $self->error( "No meta field provided to get its value." ) );
410 1         11 my $hash = $self->getDomainHash({ locale => $self->locale });
411 1   50     17 my $po = $hash->{_po} || return( $self->error( "Unable to get the po object in the locale data hash" ) );
412 1         14 return( $po->meta( $field ) );
413             }
414              
415             sub getMonthsLong
416             {
417 1     1 1 1985 my $self = shift( @_ );
418 1         5 my $ref = $self->_get_months( $self->locale );
419 1         8 return( $ref->[1] );
420             }
421              
422             sub getMonthsShort
423             {
424 1     1 1 76062 my $self = shift( @_ );
425 1         7 my $ref = $self->_get_months( $self->locale );
426 1         9 return( $ref->[0] );
427             }
428              
429             sub getNumericDict
430             {
431 1     1 1 49516 my $self = shift( @_ );
432 1         5 my $ref = $self->_get_numeric_dict( $self->locale );
433 1         41 return( $ref->[0] );
434             }
435              
436             sub getNumericPosixDict
437             {
438 1     1 1 4890 my $self = shift( @_ );
439 1         3 my $ref = $self->_get_numeric_dict( $self->locale );
440 1         5 return( $ref->[1] );
441             }
442              
443             sub getPlural
444             {
445 1     1 1 2 my $self = shift( @_ );
446 1   50     4 my $po = $self->_get_po || return( $self->error( "Unable to get the po object in the locale data hash" ) );
447 1         7 return( $po->plural );
448             }
449              
450             sub getText
451             {
452 2     2 1 6 my $self = shift( @_ );
453 2         6 my( $key, $lang ) = @_;
454 2 50 33     30 return( $self->error( "No text to get its localised equivalent was provided." ) ) if( !defined( $key ) || !length( $key ) );
455 2         10 return( $self->dngettext( $self->domain, $key, { locale => $lang }) );
456             }
457              
458             sub getTextf
459             {
460 1     1 1 2 my $self = shift( @_ );
461 1         2 my $opts = {};
462 1 50       4 $opts = pop( @_ ) if( ref( $_[-1] ) eq 'HASH' );
463 1   33     5 $opts->{lang} = $self->locale || $self->currentLang();
464 1         630 my $key = shift( @_ );
465 1         10 my $text = $self->getText( $key, $opts->{lang} );
466 1         19 return( sprintf( $text, @_ ) );
467             }
468              
469             sub gettext
470             {
471 1     1 1 4 my $self = shift( @_ );
472 1         6 return( $self->dngettext( $self->domain, shift( @_ ) ) );
473             }
474              
475 0     0 1 0 sub gettextf { return( shift->getTextf( @_ ) ); }
476              
477             sub isSupportedLanguage
478             {
479 3     3 1 15 my $self = shift( @_ );
480 3   50     24 my $lang = shift( @_ ) || return(0);
481 3   33     24 my $class = ref( $self ) || $self;
482 3         15 $lang = $self->locale_unix( $lang );
483 3         25 my $dom = $self->domain;
484 3         3188 my $repo = Module::Generic::Global->new( 'l10n' => $class );
485 3   50     558 my $l10n = $repo->get // {};
486 3 50       3194 return( $self->error( "No domain \"$dom\" set!" ) ) if( !CORE::exists( $l10n->{ $dom } ) );
487 3         34 my $dict = $l10n->{ $dom };
488 3 100       34 if( CORE::exists( $dict->{ $lang } ) )
489             {
490 2         40 return(1);
491             }
492             else
493             {
494 1         13 return(0);
495             }
496             }
497              
498 1     1 1 19 sub language { return( shift->_get_po->language ); }
499              
500 1     1 1 4 sub languageTeam { return( shift->_get_po->language_team ); }
501              
502 1     1 1 5 sub lastTranslator { return( shift->_get_po->last_translator ); }
503              
504 1     1 1 147 sub mimeVersion { return( shift->_get_po->mime_version ); }
505              
506             sub locale
507             {
508 41     41 1 47222 my $self = shift( @_ );
509 41 100       181 if( @_ )
510             {
511 2         21 my $v = shift( @_ );
512 2 50 33     851 if( !defined( $v ) || !length( $v ) )
    50          
513             {
514 0         0 return( $self->error( "No language was set." ) );
515             }
516             elsif( $v =~ /^$LOCALE_RE$/ )
517             {
518             # The user is responsible for the locale value s/he provides. We only checks s/he provides a valid locale.
519             # We normalise it.
520 2         14 my( $loc, $enc ) = split( /\./, $v, 2 );
521 2         10 $loc =~ tr/-/_/;
522 2         6 $v = $loc;
523 2 50       12 $v .= '.' . $enc if( defined( $enc ) );
524             }
525             else
526             {
527 0         0 return( $self->error( "Language provided (\"$v\") is in an unsupported format. Use something like \"en_GB\", \"en-GB\" or simply \"en\" or even \"en_GB.utf-8\"." ) );
528             }
529 2 50       24 return( $self->error( "No domain is set or it has disappeared!" ) ) if( !$self->{domain} );
530 2         28 $self->{locale} = $v;
531 2         20 my $caller = [caller(1)]->[3];
532             # We do not call textdomain upon init, because we need both domain and locale to be set first
533             # textdomain() is called directly in init()
534 2 50       14 $self->textdomain( $self->{domain} ) unless( $caller eq 'Module::Generic::init' );
535             }
536 41         242 return( $self->_set_get_scalar_as_object( 'locale' ) );
537             }
538              
539             sub locale_unix
540             {
541 31     31 1 1889 my $self = shift( @_ );
542 31   66     159 my $loc = shift( @_ ) || $self->locale;
543             # Only once
544 31 50       3322 if( $loc =~ /^$LOCALE_RE$/ )
545             {
546 31         424 ( $loc, my $enc ) = split( /\./, $loc, 2 );
547 31         186 $loc =~ tr/-/_/;
548 31 100       148 $loc .= '.' . $enc if( defined( $enc ) );
549             }
550 31         151 return( $loc );
551             }
552              
553             sub locale_web
554             {
555 10     10 1 38 my $self = shift( @_ );
556 10   33     81 my $loc = shift( @_ ) || $self->locale;
557             # Only once
558 10 50       975 if( $loc =~ /^$LOCALE_RE$/ )
559             {
560 10         65 ( $loc, my $enc ) = split( /\./, $loc, 2 );
561 10         31 $loc =~ tr/_/-/;
562 10 100       37 $loc .= '.' . $enc if( defined( $enc ) );
563             }
564 10         53 return( $loc );
565             }
566              
567             sub ngettext
568             {
569 2     2 1 2609 my $self = shift( @_ );
570 2         6 my( $msgid, $msgidPlural, $count ) = @_;
571 2         9 return( $self->dngettext( $self->domain, $msgid, $msgidPlural, $count ) );
572             }
573              
574 5     5 1 4512 sub path { return( shift->_set_get_file( 'path', @_ ) ); }
575              
576             sub plural
577             {
578 15     15 1 2251 my $self = shift( @_ );
579 15 50       50 if( @_ )
580             {
581 0         0 return( $self->_set_get_array_as_object( 'plural', @_ ) );
582             }
583             else
584             {
585 15 100       37 if( !scalar( @{$self->{plural}} ) )
  15         87  
586             {
587 1         9 $self->{plural} = $self->getPlural();
588             }
589 15         132 return( $self->_set_get_array_as_object( 'plural' ) );
590             }
591             }
592              
593 1     1 1 50247 sub pluralForms { return( shift->_get_po->plural_forms ); }
594              
595 1     1 1 6 sub po_object { return( shift->_get_po ); }
596              
597 1     1 1 598 sub poRevisionDate { return( shift->_get_po->po_revision_date ); }
598              
599 1     1 1 2635 sub potCreationDate { return( shift->_get_po->pot_creation_date ); }
600              
601 1     1 1 2205 sub projectIdVersion { return( shift->_get_po->project_id_version ); }
602              
603 1     1 1 5 sub reportBugsTo { return( shift->_get_po->report_bugs_to ); }
604              
605             sub textdomain
606             {
607 2     2 1 6 my $self = shift( @_ );
608 2   50     12 my $dom = shift( @_ ) || return( $self->error( "No domain was provided." ) );
609 2   33     33 my $class = ref( $self ) || $self;
610 2         8 my $base = $self->path;
611 2         1879 my $lang = $self->locale_unix;
612 2 50       21 my $path_po = $base->join( $base, $lang, ( $self->category ? $self->category : () ), "${dom}.po" );
613 2 50       330943 my $path_json = $base->join( $base, $lang, ( $self->category ? $self->category : () ), "${dom}.json" );
614 2 50       292710 my $path_mo = $base->join( $base, $lang, ( $self->category ? $self->category : () ), "${dom}.mo" );
615 2         338899 my $file;
616             my $po;
617              
618 2 50 33     92 if( $self->use_json && $path_json->exists )
    0          
    0          
619             {
620 2         2127 $file = $path_json;
621 2   50     45 $po = Text::PO->new( domain => $dom, use_json => 1, debug => $self->debug ) ||
622             return( $self->pass_error( Text::PO->error ) );
623 2 50       44 $po->parse2object( $file ) ||
624             return( $self->pass_error( $po->error ) );
625             }
626             elsif( $path_po->exists )
627             {
628 0         0 $file = $path_po;
629 0   0     0 $po = Text::PO->new( domain => $dom, debug => $self->debug ) ||
630             return( $self->pass_error( Text::PO->error ) );
631 0 0       0 $po->parse( $file ) ||
632             return( $self->pass_error( $po->error ) );
633             }
634             elsif( $path_mo->exists )
635             {
636 0         0 $file = $path_mo;
637 0   0     0 my $mo = Text::PO::MO->new( $file, { domain => $dom, debug => $self->debug }) ||
638             return( $self->pass_error( Text::PO::MO->error ) );
639 0   0     0 $po = $mo->as_object ||
640             return( $self->pass_error( $po->error ) );
641             }
642             else
643             {
644 0         0 return( $self->error( "No data file could be found for \"$dom\" for either json, po, or mo file." ) );
645             }
646 2         18 my $repo = Module::Generic::Global->new( 'l10n' => $class );
647 2         355 $repo->lock;
648 2   100     37 my $l10n = $repo->get // {};
649 2 100       1054 $l10n->{ $dom } = {} if( ref( $l10n->{ $dom } ) ne 'HASH' );
650 2 50       24 my $dict = $l10n->{ $dom }->{ $lang } = {} if( ref( $l10n->{ $dom }->{ $lang } ) ne 'HASH' );
651 2         31 $dict->{_po} = $po;
652             $po->elements->foreach(sub
653             {
654 18     18   18241 my $ref = shift( @_ );
655 18         81 $dict->{ $ref->{msgid} } = $ref;
656 2         15 });
657 2         475 $repo->set( $l10n );
658 2         2392 $repo->unlock;
659 2         86 return( $self );
660             }
661              
662 3     3 1 2011 sub use_json { return( shift->_set_get_boolean( 'use_json', @_ ) ); }
663              
664             sub _get_days
665             {
666 4     4   4201 my $self = shift( @_ );
667 4         12 my $locale = shift( @_ );
668 4         53 my $oldlocale = POSIX::setlocale( &POSIX::LC_ALL );
669 4         24 my $short = $self->new_array;
670 4         3426 my $long = $self->new_array;
671              
672 4 50       3199 POSIX::setlocale( &POSIX::LC_ALL, $locale ) if( defined( $locale ) );
673              
674 4         106 for (my $i = 1; $i <= 7; $i++)
675             {
676             # my $const = "I18N::Langinfo::ABDAY_${i}";
677 28         241 my $const = I18N::Langinfo->can( "ABDAY_${i}" );
678             # $short->[$i-1] = langinfo( &$const );
679 28         188 $short->[$i-1] = langinfo( $const->() );
680             }
681 4         17 for (my $i = 1; $i <= 7; $i++)
682             {
683             # my $const = "I18N::Langinfo::DAY_${i}";
684 28         191 my $const = I18N::Langinfo->can( "DAY_${i}" );
685             # $long->[$i-1] = langinfo( &$const );
686 28         169 $long->[$i-1] = langinfo( $const->() );
687             }
688              
689 4 50       43 POSIX::setlocale( &POSIX::LC_ALL, $oldlocale) if( defined( $locale ) );
690              
691 4         18 return( [ $short, $long ] );
692             }
693              
694             sub _get_months
695             {
696 2     2   2109 my $self = shift( @_ );
697 2         8 my $locale = shift( @_ );
698 2         242 my $oldlocale = POSIX::setlocale( &POSIX::LC_ALL );
699 2         23 my $short = $self->new_array;
700 2         1725 my $long = $self->new_array;
701              
702 2 50       1636 POSIX::setlocale( &POSIX::LC_ALL, $locale ) if( defined( $locale ) );
703              
704 2         46 for( my $i = 1; $i <= 12; $i++ )
705             {
706             # my $const = "I18N::Langinfo::ABMON_${i}";
707             # $short->[$i-1] = langinfo( &$const );
708 24         284 my $const = I18N::Langinfo->can( "ABMON_${i}" );
709 24         163 $short->[$i-1] = langinfo( $const->() );
710             }
711 2         9 for( my $i = 1; $i <= 12; $i++ )
712             {
713             # my $const = "I18N::Langinfo::MON_${i}";
714             # $long->[$i-1] = langinfo( &$const );
715 24         185 my $const = I18N::Langinfo->can( "MON_${i}" );
716 24         121 $long->[$i-1] = langinfo( $const->() );
717             }
718              
719 2 50       22 POSIX::setlocale( &POSIX::LC_ALL, $oldlocale) if( defined( $locale ) );
720              
721 2         9 return( [ $short, $long ] );
722             }
723              
724             sub _get_numeric_dict
725             {
726 2     2   1598 my $self = shift( @_ );
727 2         5 my $locale = shift( @_ );
728 2         24 my $oldlocale = POSIX::setlocale( &POSIX::LC_ALL );
729 2 50       18 POSIX::setlocale( &POSIX::LC_ALL, $locale) if( defined( $locale ) );
730 2         66 my $lconv = POSIX::localeconv();
731 2 50       19 POSIX::setlocale( &POSIX::LC_ALL, $oldlocale) if( defined( $locale ) );
732 2         26 my $def = $self->new_hash;
733             @$def{qw( currency decimal int_currency negative_sign thousand precision )} =
734 2         2812 @$lconv{qw( currency_symbol decimal_point int_curr_symbol negative_sign thousands_sep frac_digits )};
735 2     2   21 use utf8;
  2         5  
  2         19  
736 2 50 33     223 $def->{currency} = '€' if( CORE::exists( $def->{currency} ) && defined( $def->{currency} ) && $def->{currency} eq 'EUR' );
      33        
737 2 50 33     159 $lconv->{currency_symbol} = '€' if( CORE::exists( $lconv->{currency_symbol} ) && defined( $lconv->{currency_symbol} ) && $lconv->{currency_symbol} eq 'EUR' );
      33        
738 2 50 33     45 $lconv->{grouping} = unpack( "C*", $lconv->{grouping} ) if( CORE::exists( $lconv->{grouping} ) && defined( $lconv->{grouping} ) );
739 2 50 33     15 $lconv->{mon_grouping} = unpack( "C*", $lconv->{mon_grouping} ) if( CORE::exists( $lconv->{mon_grouping} ) && defined( $lconv->{mon_grouping} ) );
740 2         35 $lconv = $self->new_hash( $lconv );
741 2         3333 return( [ $def, $lconv ] );
742             }
743              
744             sub _get_po
745             {
746 15     15   43 my $self = shift( @_ );
747 15         72 my $hash = $self->getDomainHash({ locale => $self->locale });
748 15         181 return( $hash->{_po} );
749             }
750              
751             # NOTE: Text::PO::String class
752             {
753             package
754             Text::PO::String;
755             BEGIN
756 0         0 {
757 2     2   804 use strict;
  2         6  
  2         60  
758 2     2   12 use warnings;
  2         4  
  2         170  
759 2     2   15 use parent qw( Module::Generic );
  2         22  
  2         18  
760 2     2   267 use vars qw( $VERSION );
  2         41  
  2         235  
761 2     2   358 our $VERSION = 'v0.1.0';
762             use overload (
763             '""' => 'as_string',
764 0     0   0 'bool' => sub{1},
765 2         24 fallback => 1,
766 2     2   15 );
  2         36  
767             };
768              
769 2     2   44 use strict;
  2         7  
  2         86  
770 2     2   13 use warnings;
  2         2  
  2         642  
771              
772             sub init
773             {
774 4     4   504 my $self = shift( @_ );
775 4         27 my $value = shift( @_ );
776 4         11 my $locale = shift( @_ );
777 4         90 $self->{locale} = $locale;
778 4         16 $self->{value} = $value;
779 4         30 $self->SUPER::init( @_ );
780 4         379 return( $self );
781             }
782              
783 4     4   2768 sub as_string { return( shift->value->scalar ); }
784              
785 0     0   0 sub locale { return( shift->_set_get_scalar_as_object( 'locale', @_ ) ); }
786              
787 4     4   54 sub value { return( shift->_set_get_scalar_as_object( 'value', @_ ) ); }
788              
789 0     0     sub TO_JSON { return( shift->as_string ); }
790             }
791              
792             1;
793             # NOTE: POD
794             __END__
795              
796             =encoding utf-8
797              
798             =head1 NAME
799              
800             Text::PO::Gettext - Object-oriented GNU Gettext-style implementation
801              
802             =head1 SYNOPSIS
803              
804             use Text::PO::Gettext;
805              
806             # Basic usage: one domain, one locale
807             my $po = Text::PO::Gettext->new(
808             path => '/home/joe/locale', # path to where the list of locales directories are
809             domain => 'com.example.api',
810             locale => 'fr_FR', # optional, falls back to environment; _ or - does not matter
811             # use_json => 1, # if you use JSON exports from Text::PO
812             # category => 'LC_MESSAGES', # optional, defaults to LC_MESSAGES
813             ) || die( Text::PO::Gettext->error );
814              
815             # Simple lookup
816             my $hello = $po->gettext('Hello world');
817              
818             # Text::PO::String objects stringify to the translated text
819             say $hello; # "Bonjour le monde"
820             say $hello->value; # same
821             say $hello->locale; # "fr_FR"
822              
823             # Plural form lookup
824             my $n = 3;
825             my $apples = $po->ngettext(
826             '%d apple', # singular
827             '%d apples', # plural
828             $n,
829             );
830             printf "$apples\n", $n; # formatted with %d, still Text::PO::String
831              
832             # Typical web service / PSGI pattern
833             my $msg = $po->gettext('Invalid parameter');
834              
835             my $body = { error => "$msg" }; # stringification
836             my $res = [
837             200,
838             [
839             'Content-Type' => 'application/json; charset=utf-8',
840             'Content-Language' => $msg->locale, # <- effective locale
841             ],
842             [ encode_json($body) ],
843             ];
844              
845             # Plural forms
846             my $count = 3;
847             my $files = $po->ngettext('%d file', '%d files', $count);
848             printf "$files\n", $count; # "3 fichiers"
849              
850             # Per-object locale: two locales loaded simultaneously
851             my $fr = Text::PO::Gettext->new(
852             domain => 'com.example.api',
853             locale => 'fr_FR',
854             path => '/some/where/locale',
855             );
856              
857             my $en = Text::PO::Gettext->new(
858             domain => 'com.example.api',
859             locale => 'en_GB',
860             path => '/some/where/locale',
861             );
862              
863             my $msg_fr = $fr->gettext( 'Welcome' );
864             my $msg_en = $en->gettext( 'Welcome' );
865              
866             say "FR: $msg_fr"; # "FR: Bienvenue"
867             say "EN: $msg_en"; # "EN: Welcome"
868              
869             # Using Text::PO::String to set Content-Language in a web service
870             my $title = $po->gettext( 'Page title' );
871              
872             $res->header( 'Content-Language' => $title->locale );
873             $res->body( $title->value );
874              
875             # Switching locale on an existing object
876             $po->locale( 'fr_FR' ); # internally normalised
877             my $bye = $po->gettext( 'Goodbye' );
878              
879             # JSON-based catalogues instead of PO/MO
880             my $json_po = Text::PO::Gettext->new(
881             domain => 'com.example.api',
882             locale => 'ja_JP',
883             path => '/var/www/locale',
884             use_json => 1,
885             );
886              
887             my $label = $json_po->gettext( 'Log in' );
888              
889             =head1 VERSION
890              
891             v0.3.2
892              
893             =head1 DESCRIPTION
894              
895             This module provides an object-oriented interface to gettext-style localisation data stored as C<.po>, C<.mo>, or C<.json> files.
896              
897             The conventional way to use GNU gettext is to set the global environment variable C<LANGUAGE> (not C<LANG> by the way. GNU gettext only uses C<LANGUAGE>), then set the L<POSIX/setlocale> to the language such as:
898              
899             use Locale::gettext;
900             use POSIX ();
901             POSIX::setlocale( &POSIX::LC_ALL, 'ja_JP' );
902             my $d = Locale::gettext->domain( 'com.example.api' );
903              
904             And then in your application, you would write a statement like:
905              
906             print $d->get( 'Hello!' );
907              
908             Or possibly using direct access to the C function:
909              
910             use Locale::gettext;
911             use POSIX ();
912             POSIX::setlocale( &POSIX::LC_ALL, 'ja_JP' );
913             textdomain( 'com.example.api' );
914              
915             And then:
916              
917             print gettext( 'Hello!' );
918              
919             See L<Locale::gettext> for more on this.
920              
921             This works fine, but has the inconvenience that it uses the global C<LANGUAGE> environment variable and makes it less than subpar as to the necessary flexibility when using multiple domains and flipping back and forth among locales.
922              
923             Thus comes a more straightforward object-oriented interface offered by this module.
924              
925             So, L<Text::PO::Gettext> allows you to create multiple independent localisation objects, each with its own domain, locale, and data directory. This makes it straightforward to:
926              
927             =over 4
928              
929             =item *
930              
931             serve multiple users with different locales in the same process,
932              
933             =item *
934              
935             switch locale per request without touching global environment variables, and
936              
937             =item *
938              
939             work comfortably under threaded environments, mod_perl, daemons, etc.
940              
941             =back
942              
943             A typical directory layout might look like:
944              
945             /some/where/locale/
946             en_GB/
947             LC_MESSAGES/
948             com.example.api.po
949             com.example.api.mo
950             com.example.api.json
951             ja_JP/
952             LC_MESSAGES/
953             com.example.api.po
954             com.example.api.json
955              
956             Based on the options you pass (C<domain>, C<locale>, C<path>, C<use_json>), the module will look for a suitable localisation file in the order:
957              
958             =over 4
959              
960             =item *
961              
962             JSON file (C<.json>) if C<use_json> is true and such a file exists,
963              
964             =item *
965              
966             PO file (C<.po>) if present,
967              
968             =item *
969              
970             MO file (C<.mo>) as a fallback.
971              
972             =back
973              
974             Thus, you instantiate an object, passing the domain, the locale and the filesystem path where the locale data resides.
975              
976             my $po = Text::PO::Gettext->new(
977             domain => 'com.example.api',
978             locale => 'ja_JP',
979             path => '/some/where/locale'
980             );
981             print $po->gettext( 'Hello!' );
982              
983             This will load into memory the locale data whether they are stored as C<.po>, C<.mo> or even C<.json> file, thus making calls to L</gettext> super fast since they are in memory.
984              
985             More than one locale can be loaded, each with its own L<Text::PO::Gettext> object
986              
987             This distribution comes with its Javascript library equivalent. See the C<share> folder alone with its own test units.
988              
989             Also, there is a script in C<scripts> that can be used to transcode C<.po> or C<.mo> files into json format and vice versa.
990              
991             Still, it is better to convert the original C<.po> files to json using the C<po.pl> utility that comes in this L<Text::PO> distribution since it would allow the standalone JavaScript library to read json-based po files. For example:
992              
993             ./po.pl --as-json --output /home/joe/www/locale/ja_JP/LC_MESSAGES/com.example.api.json ./ja_JP.po
994              
995             This api supports locale that use hyphens or underscore in them such as C<en-GB> or C<en_GB>. You can use either, it will be converted internally.
996              
997             All translated strings returned by this module are instances of L<Text::PO::String>. L<Text::PO::String> objects are tiny wrappers around the translated value. They:
998              
999             =over 4
1000              
1001             =item *
1002              
1003             Stringify transparently to the translated text.
1004              
1005             =item *
1006              
1007             Provide a C<locale> method returning the effective locale used for that lookup (for example C<en_GB> or C<fr_FR>).
1008              
1009             =item *
1010              
1011             Provide a C<value> method returning the translated text as a plain scalar.
1012              
1013             =back
1014              
1015             This is particularly handy for web services and APIs, where you may want to inspect the locale actually used for a given message and, for example, set the C<Content-Language> response header accordingly:
1016              
1017             my $msg = $po->gettext( 'Error: invalid input' );
1018              
1019             $res->header( 'Content-Language' => $msg->locale );
1020             $res->body( $msg->value );
1021              
1022             You can safely treat C<Text::PO::String> objects as regular strings in most contexts; they are designed to be drop-in replacements for plain scalars from the caller’s point of view, while still carrying useful metadata.
1023              
1024             =head1 CONSTRUCTOR
1025              
1026             =head2 new
1027              
1028             my $po = Text::PO::Gettext->new(
1029             domain => 'com.example.api',
1030             locale => 'ja_JP',
1031             path => '/some/where/locale',
1032             category => 'LC_MESSAGES', # optional
1033             use_json => 1, # optional
1034             plural => [ $n, $expr ], # optional
1035             debug => 1, # optional
1036             );
1037              
1038             Creates a new C<Text::PO::Gettext> object.
1039              
1040             Takes the following options and returns a Gettext object.
1041              
1042             =over 4
1043              
1044             =item * C<category>
1045              
1046             This is optional.
1047              
1048             If I<category> is defined, such as C<LC_MESSAGES> (by default), it will be used when building the I<path>.
1049              
1050             Other possible category values are: C<LC_CTYPE>, C<LC_NUMERIC>, C<LC_TIME>, C<LC_COLLATE>, C<LC_MONETARY>
1051              
1052             See L<GNU documentation for more information|https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html> and L<perllocale/"LOCALE CATEGORIES">
1053              
1054             On the web, using the path is questionable.
1055              
1056             See the L<GNU documentation|https://www.gnu.org/software/libc/manual/html_node/Using-gettextized-soft
1057             ware.html> for more information on this.
1058              
1059             =item * C<domain>
1060              
1061             This is required.
1062              
1063             The portable object domain, such as C<com.example.api>
1064              
1065             This typically corresponds to the base filename of your PO/MO/JSON files.
1066              
1067             =item * C<locale>
1068              
1069             This is required.
1070              
1071             The locale, such as C<ja_JP>, or C<en>, or it could even contain a hyphen instead of an underscore, such as C<en-GB>. Internally, though, this will be converted to underscore.
1072              
1073             =item * C<path>
1074              
1075             This is required.
1076              
1077             Base filesystem path where the localisation files are stored.
1078              
1079             This is used to form a path along with the locale string. For example, with a locale of C<ja_JP> and a domain of C<com/example.api>, if the path were C</locale>, the data po json data would be fetched from C</locale/
1080             ja_JP/LC_MESSAGES/com.example.api.json>
1081              
1082             =item * C<plural>
1083              
1084             This is optional.
1085              
1086             An array reference C<[ $n, $expr ]> defining plural forms for the current domain and locale. This is normally derived from the PO metadata (C<Plural-Forms>) but can be overridden if needed.
1087              
1088             =item * C<use_json>
1089              
1090             Optional boolean value.
1091              
1092             If true, PO data will be loaded from JSON files produced by L<Text::PO> instead of regular PO/MO files.
1093              
1094             This is particularly useful when working with the JavaScript companion library provided by the L<Text::PO> distribution.
1095              
1096             =back
1097              
1098             The constructor does not immediately load the catalogue; data is pulled in when you first call L</textdomain> or any of the lookup methods, which internally ensure the domain has been loaded.
1099              
1100             Returns the newly created L<Text::PO::Gettext> object or upon error, it sets an L<error object|Module::Generic::Exception>, and returns C<undef> in scalar context, or an empty list in list context.
1101              
1102             =head1 METHODS
1103              
1104             =head2 addItem
1105              
1106             This takes a C<locale>, a message id and its localised version and it will add this to the current dictionary for the current domain.
1107              
1108             $po->addItem( 'ja_JP', 'Hello!' => "今日は!" );
1109              
1110             =head2 category
1111              
1112             The category to use. This defaults to C<LC_MESSAGES>, but if you prefer you can nix its use by making it undefined, or empty:
1113              
1114             my $po = Text::PO::Gettext->new(
1115             category => '',
1116             domain => 'com.example.api',
1117             locale => 'ja_JP',
1118             path => '/some/where/locale'
1119             );
1120             # Setting category to empty string will have the module get the po data
1121             # under C</some/where/locale/ja_JP/com.example.api.json> for example.
1122             print $po->gettext( 'Hello!' );
1123              
1124             =head2 charset
1125              
1126             Returns a string containing the value of the charset encoding as defined in the C<Content-Type> header.
1127              
1128             $po->charset()
1129              
1130             =head2 contentEncoding
1131              
1132             Returns a string containing the value of the header C<Content-Encoding>.
1133              
1134             $po->contentEncoding();
1135              
1136             =head2 contentType
1137              
1138             Returns a string containing the value of the header C<Content-Type>.
1139              
1140             $po->contentType(); # text/plain; charset=utf-8
1141              
1142             =head2 currentLang
1143              
1144             Return the current globally used locale. This is the value found in environment variables C<LANGUAGE> or C<LANG>. Note that GNU gettext only recognises C<LANGUAGE>
1145              
1146             and thus, this is different from the C<locale> set in the Gettext class object using </setLocale> or upon class object instantiation.
1147              
1148             =head2 dgettext
1149              
1150             This is like L</gettext>, but takes a specific domain and a message ID and returns the equivalent localised string if any, otherwise the original message id.
1151              
1152             $po->dgettext( 'com.example.auth', 'Please enter your e-mail address' );
1153             # Assuming the locale currently set is ja_JP, this would return:
1154             # 電子メールアドレスをご入力下さい。
1155              
1156             =head2 dngettext
1157              
1158             Same as L</ngettext>, but takes also a domain as first argument. For example:
1159              
1160             $po->ngettext( 'com.example.auth', '%d comment awaiting moderation', '%d comments awaiting moderation', 12 );
1161             # Assuming the locale is ru_RU, this would return:
1162             # %d комментариев ожидают проверки
1163              
1164             Note that as of version C<v0.5.0>, this returns a C<Text::PO::String>, which is lightweight and stringifies automatically. It provides the benefit of tagging the string with the locale attached to it.
1165              
1166             Thus, in the example above, the resulting C<Text::PO::String> would have its method C<locale> value set to C<ru_RU>, and you could do:
1167              
1168             my $localised = $po->ngettext( 'com.example.auth', '%d comment awaiting moderation', '%d comments awaiting moderation', 12 );
1169             say "Locale for this string is: ", $localised->locale;
1170              
1171             If no locale string was found, C<locale> would be undefined.
1172              
1173             =head2 domain
1174              
1175             Sets or gets the domain.
1176              
1177             $po->domain( 'com.example.api' );
1178              
1179             By doing so, this will call L</textdomain> and load the associated data from file, if any are found.
1180              
1181             =head2 exists
1182              
1183             Provided with a locale, and this returns true if the locale exists in the current domain, or false otherwise.
1184              
1185             =head2 fetchLocale
1186              
1187             Given an original string (msgid), this returns an array of <span> html element each for one language and its related localised content. For example:
1188              
1189             my $array = $po->fetchLocale( "Hello!" );
1190             # Returns:
1191             <span lang="de-DE">Grüß Gott!</span>
1192             <span lang="fr-FR">Salut !</span>
1193             <span lang="ja-JP">今日は!</span>
1194             <span lang="ko-KR">안녕하세요!</span>
1195              
1196             This is designed to be added to the html, and based on C<lang> attribute of the C<html> tag, and using the following css trick, this will automatically display the right localised data:
1197              
1198             [lang=de-DE] [lang=en-GB],
1199             [lang=de-DE] [lang=fr-FR],
1200             [lang=de-DE] [lang=ja-JP],
1201             [lang=de-DE] [lang=ko-KR],
1202             [lang=en-GB] [lang=de-DE],
1203             [lang=en-GB] [lang=fr-FR],
1204             [lang=en-GB] [lang=ja-JP],
1205             [lang=en-GB] [lang=ko-KR],
1206             [lang=fr-FR] [lang=de-DE],
1207             [lang=fr-FR] [lang=en-GB],
1208             [lang=fr-FR] [lang=ja-JP],
1209             [lang=fr-FR] [lang=ko-KR],
1210             [lang=ja-JP] [lang=de-DE],
1211             [lang=ja-JP] [lang=en-GB]
1212             [lang=ja-JP] [lang=fr-FR],
1213             [lang=ja-JP] [lang=ko-KR]
1214             {
1215             display: none !important;
1216             visibility: hidden !important;
1217             }
1218              
1219             =head2 getDataPath
1220              
1221             This takes no argument and will check for the environment variables C<TEXTDOMAINDIR>. If found, it will use this in lieu of the I<path> option used during object instantiation.
1222              
1223             It returns the value found. This is just a helper method and does not affect the value of the I<path> property set during object instantiation.
1224              
1225             =head2 getDaysLong
1226              
1227             Returns an array reference containing the 7 days of the week in their long representation.
1228              
1229             my $ref = $po->getDaysLong();
1230             # Assuming the locale is fr_FR, this would yield
1231             print $ref->[0], "\n"; # dim.
1232              
1233             =head2 getDaysShort
1234              
1235             Returns an array reference containing the 7 days of the week in their short representation.
1236              
1237             my $ref = $po->getDaysShort();
1238             # Assuming the locale is fr_FR, this would yield
1239             print $ref->[0], "\n"; # dimanche
1240              
1241             =head2 getDomainHash
1242              
1243             This takes an optional hash of parameters and return the global hash dictionary used by this class to store the localised data.
1244              
1245             # Will use the default domain as set in po.domain
1246             my $data = $po->getDomainHash();
1247             # Explicitly specify another domain
1248             my $data = $po->getDomainHash( domain => "net.example.api" );
1249             # Specify a domain and a locale
1250             my $po = $po->getDomainHash( domain => "com.example.api", locale => "ja_JP" );
1251              
1252             Possible options are:
1253              
1254             =over 4
1255              
1256             =item * C<domain> The domain for the data, such as C<com.example.api>
1257              
1258             =item * C<locale> The locale to return the associated dictionary.
1259              
1260             =back
1261              
1262             =head2 getLangDataPath
1263              
1264             Contrary to its JavaScript equivalent, this takes no parameter. It returns the value of the environment variable C<TEXTLOCALEDIR> if found.
1265              
1266             This is used internally during object instantiation when the I<path> parameter is not provided.
1267              
1268             =head2 getLanguageDict
1269              
1270             Provided with a locale, such as C<ja_JP> and this will return the dictionary for the current domain and the given locale.
1271              
1272             =head2 getLocale
1273              
1274             Returns the locale set for the current object, such as C<fr_FR> or C<ja_JP>
1275              
1276             Locale returned are always formatted for the server-side, which means having an underscore rather than an hyphen like in the web environment.
1277              
1278             =head2 getLocales
1279              
1280             Provided with a C<msgid> (i.e. an original text) and this will call L</fetchLocale> and return those C<span> tags as a string containing their respective localised content, joined by a new line
1281              
1282             =head2 getLocalesf
1283              
1284             This is similar to L</getLocale>, except that it does a sprintf internally before returning the resulting value.
1285              
1286             =head2 getMetaKeys
1287              
1288             Returns an array of the meta field names used.
1289              
1290             =head2 getMetaValue
1291              
1292             Provided with a meta field name and this returns its corresponding value.
1293              
1294             =head2 getMonthsLong
1295              
1296             Returns an array reference containing the 12 months in their long representation.
1297              
1298             my $ref = $po->getMonthsLong();
1299             # Assuming the locale is fr_FR, this would yield
1300             print $ref->[0], "\n"; # janvier
1301              
1302             =head2 getMonthsShort
1303              
1304             Returns an array reference containing the 12 months in their short representation.
1305              
1306             my $ref = $po->getMonthsShort();
1307             # Assuming the locale is fr_FR, this would yield
1308             print $ref->[0], "\n"; # janv.
1309              
1310             =head2 getNumericDict
1311              
1312             Returns an hash reference containing the following properties:
1313              
1314             my $ref = $po->getNumericDict();
1315              
1316             =over 4
1317              
1318             =item * C<currency> string
1319              
1320             Contains the usual currency symbol, such as C<€>, or C<$>, or C<¥>
1321              
1322             =item * C<decimal> string
1323              
1324             Contains the character used to separate decimal. In English speaking countries, this would typically be a dot.
1325              
1326             =item * C<int_currency> string
1327              
1328             Contains the 3-letters international currency symbol, such as C<USD>, or C<EUR> or C<JPY>
1329              
1330             =item * C<negative_sign> string
1331              
1332             Contains the negative sign used for negative number
1333              
1334             =item * C<precision> integer
1335              
1336             An integer whose value represents the fractional precision allowed for monetary context.
1337              
1338             For example, in Japanese, this value would be 0 while in many other countries, it would be 2.
1339              
1340             =item * C<thousand> string
1341              
1342             Contains the character used to group and separate thousands.
1343              
1344             For example, in France, it would be a space, such as :
1345              
1346             1 000 000,00
1347              
1348             While in English countries, including Japan, it would be a comma :
1349              
1350             1,000,000.00
1351              
1352             =back
1353              
1354             =head2 getNumericPosixDict
1355              
1356             Returns the full hash reference returned by L<POSIX/lconv>. It contains the following properties:
1357              
1358             Here the values shown as example are for the locale C<en_US>
1359              
1360             =over 4
1361              
1362             =item * C<currency_symbol> string
1363              
1364             The local currency symbol: C<$>
1365              
1366             =item * C<decimal_point> string
1367              
1368             The decimal point character, except for currency values, cannot be an empty string: C<.>
1369              
1370             =item * C<frac_digits> integer
1371              
1372             The number of digits after the decimal point in the local style for currency value: 2
1373              
1374             =item * C<grouping>
1375              
1376             The sizes of the groups of digits, except for currency values. unpack( "C*", $grouping ) will give the number
1377              
1378             =item * C<int_curr_symbol> string
1379              
1380             The standardized international currency symbol: C<USD>
1381              
1382             =item * C<int_frac_digits> integer
1383              
1384             The number of digits after the decimal point in an international-style currency value: 2
1385              
1386             =item * C<int_n_cs_precedes> integer
1387              
1388             Same as n_cs_precedes, but for internationally formatted monetary quantities: 1
1389              
1390             =item * C<int_n_sep_by_space> integer
1391              
1392             Same as n_sep_by_space, but for internationally formatted monetary quantities: 1
1393              
1394             =item * C<int_n_sign_posn> integer
1395              
1396             Same as n_sign_posn, but for internationally formatted monetary quantities: 1
1397              
1398             =item * C<int_p_cs_precedes> integer
1399              
1400             Same as p_cs_precedes, but for internationally formatted monetary quantities: 1
1401              
1402             =item * C<int_p_sep_by_space> integer
1403              
1404             Same as p_sep_by_space, but for internationally formatted monetary quantities: 1
1405              
1406             =item * C<int_p_sign_posn> integer
1407              
1408             Same as p_sign_posn, but for internationally formatted monetary quantities: 1
1409              
1410             =item * C<mon_decimal_point> string
1411              
1412             The decimal point character for currency values: C<.>
1413              
1414             =item * C<mon_grouping>
1415              
1416             Like grouping but for currency values.
1417              
1418             =item * C<mon_thousands_sep> string
1419              
1420             The separator for digit groups in currency values: C<,>
1421              
1422             =item * C<n_cs_precedes> integer
1423              
1424             Like p_cs_precedes but for negative values: 1
1425              
1426             =item * C<n_sep_by_space> integer
1427              
1428             Like p_sep_by_space but for negative values: 0
1429              
1430             =item * C<n_sign_posn> integer
1431              
1432             Like p_sign_posn but for negative currency values: 1
1433              
1434             =item * C<negative_sign> string
1435              
1436             The character used to denote negative currency values, usually a minus sign: C<->
1437              
1438             =item * C<p_cs_precedes> integer
1439              
1440             1 if the currency symbol precedes the currency value for nonnegative values, 0 if it follows: 1
1441              
1442             =item * C<p_sep_by_space> integer
1443              
1444             1 if a space is inserted between the currency symbol and the currency value for nonnegative values, 0 otherwise: 0
1445              
1446             =item * C<p_sign_posn> integer
1447              
1448             The location of the positive_sign with respect to a nonnegative quantity and the currency_symbol, coded as follows:
1449              
1450             0 Parentheses around the entire string.
1451             1 Before the string.
1452             2 After the string.
1453             3 Just before currency_symbol.
1454             4 Just after currency_symbol.
1455              
1456             =item * C<positive_sign> string
1457              
1458             The character used to denote nonnegative currency values, usually the empty string
1459              
1460             =item * C<thousands_sep> string
1461              
1462             The separator between groups of digits before the decimal point, except for currency values: C<,>
1463              
1464             =back
1465              
1466             =head2 getPlural
1467              
1468             my $str = $po->getPlural( $singular, $plural, $count, [ $domain ] );
1469              
1470             Calls L<Text::PO/plural> and returns an array object (L<Module::Generic::Array>) with 2 elements.
1471              
1472             Internal helper used by L</ngettext> and L</dngettext>. It applies the plural expression associated with the current (or given) domain/locale, selects the correct form, and returns a C<Text::PO::String>.
1473              
1474             See L<Text::PO/plural> for more details.
1475              
1476             =head2 getText
1477              
1478             my $str = $po->getText( $msgid );
1479              
1480             Internal workhorse used by L</gettext> and domain-aware variants.
1481              
1482             Provided with an original string, and this will return its localised equivalent if it exists, or by default, it will return the original string.
1483              
1484             Returns a C<Text::PO::String> as described elsewhere.
1485              
1486             =head2 getTextf
1487              
1488             my $str = $po->getTextf( $msgid, @args );
1489              
1490             Provided with an original string, and this will get its localised equivalent that wil be used as a template for the sprintf function. The resulting formatted localised content will be returned.
1491              
1492             =head2 gettext
1493              
1494             Provided with a C<msgid> represented by a string, and this return a localised version of the string, under the current domain and locale, if any is found and is translated, otherwise returns the C<msgid> that was provided.
1495              
1496             $po->gettext( "Hello" );
1497             # With locale of fr_FR, this would return "Bonjour"
1498              
1499             See the global function L</_> for more information.
1500              
1501             Note that as of version C<v0.5.0> of L<Text::PO>, this returns a C<Text::PO::String>, which is lightweight and stringifies automatically. It provides the benefit of tagging the string with the locale attached to it.
1502              
1503             Thus, in the example above, the resulting C<Text::PO::String> would have its method C<locale> value set to C<fr_FR>, and you could do:
1504              
1505             my $localised = $po->gettext( "Hello" );
1506             say "Locale for this string is: ", $localised->locale;
1507              
1508             If no locale string was found, C<locale> would be undefined.
1509              
1510             =head2 gettextf
1511              
1512             my $str = $po->gettextf( $format, @args );
1513              
1514             This is an alias to L</getTextf>
1515              
1516             Like L</gettext>, but intended for printf-style formatting. It looks up C<$format> as the message id, then applies C<sprintf> to fill in C<@args>.
1517              
1518             The returned object is still a L<Text::PO::String>; formatting is performed on the underlying string.
1519              
1520             =head2 isSupportedLanguage
1521              
1522             if( $po->isSupportedLanguage( 'ja_JP' ) )
1523             {
1524             # Do something
1525             }
1526              
1527             Provided with a locale such as C<fr-FR> or C<ja_JP> no matter whether an underscore or a dash is used, and this will return true if the locale has already been loaded and thus is supported. False otherwise.
1528              
1529             =head2 language
1530              
1531             Returns a string containing the value of the GNU PO file header C<Language>.
1532              
1533             $po->language();
1534              
1535             =head2 languageTeam
1536              
1537             Returns a string containing the value of the GNU PO file header C<Language-Team>.
1538              
1539             $po->languageTeam();
1540              
1541             =head2 lastTranslator
1542              
1543             Returns a string containing the value of the GNU PO file header C<Last-Translator>.
1544              
1545             $po->lastTranslator();
1546              
1547             =head2 locale
1548              
1549             $po->locale( 'fr-FR' ); # or 'fr_FR' either is the same
1550             my $locale = $po->locale;
1551              
1552             Returns the locale set in the object as a L<scalar object|Module::Generic::Scalar>. if sets, this will trigger the (re)load of po data by calling L</textdomain>
1553              
1554             Note that this does not modify global environment variables such as C<LANGUAGE> or any process-wide locale; the change is strictly per-object.
1555              
1556             =head2 locale_unix
1557              
1558             my $unix_locale = $po->locale_unix; # en_GB
1559             my $norm = $po->locale_unix( 'en-gb' ); # becomes en_GB
1560              
1561             Provided with a locale, such as C<en-GB> and this will return its equivalent formatted for server-side such as C<en_GB>
1562              
1563             =head2 locale_web
1564              
1565             my $unix_locale = $po->locale_web; # en-GB
1566             my $norm = $po->locale_web( 'en_gb' ); # becomes en-GB
1567              
1568             Provided with a locale, such as C<en_GB> and this will return its equivalent formatted for the web such as C<en-GB>
1569              
1570             =head2 mimeVersion
1571              
1572             Returns a string containing the value of the header C<MIME-Version>.
1573              
1574             $po->mimeVersion();
1575              
1576             =head2 ngettext
1577              
1578             This perform plural-aware lookup.
1579              
1580             Takes an original string (a.k.a message id), the plural version of that string, and an unsigned integer representing the applicable count, and this selects the appropriate translation according to the plural rules for the current locale/domain. For example:
1581              
1582             $po->ngettext( '%d comment awaiting moderation', '%d comments awaiting moderation', 12 );
1583             # Assuming the locale is ru_RU, this would return:
1584             # %d комментариев ожидают проверки
1585              
1586             As with L</gettext>, the result is a C<Text::PO::String> which you can print directly or pass to C<sprintf>:
1587              
1588             printf $po->ngettext(
1589             '%d apple',
1590             '%d apples',
1591             $count,
1592             ), $count;
1593              
1594             =head2 path
1595              
1596             Sets or gets the filesystem path to the base directory containing the locale data:
1597              
1598             $po->path( '/locale' ); # /locale contains en_GB/LC_MESSAGES/com.example.api.mo for example
1599              
1600             =head2 plural
1601              
1602             my $plural_def = $po->plural;
1603             $po->plural( [ $n, $expr ] );
1604              
1605             Sets or gets the definition for plural for the current domain and locale. Usually you do not need to set this manually; it is derived from the PO metadata (C<Plural-Forms>).
1606              
1607             It takes and returns an array reference of 2 elements:
1608              
1609             =over 4
1610              
1611             =item 0. An integer representing the various plural forms available, starting from 1
1612              
1613             =item 1. An expression to be evaluated resulting in an offset for the right plural form. For example:
1614              
1615             n>1
1616              
1617             or more complex for Russian:
1618              
1619             (n==1) ? 0 : (n%10==1 && n%100!=11) ? 3 : ((n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20)) ? 1 : 2)
1620              
1621             =back
1622              
1623             =head2 pluralForms
1624              
1625             Returns a string containing the value of the header C<Plural-Forms>.
1626              
1627             $po->pluralForms();
1628              
1629             =head2 po_object
1630              
1631             Returns the L<Text::PO> object used.
1632              
1633             =head2 poRevisionDate
1634              
1635             Returns a string containing the value of the header C<PO-Revision-Date>.
1636              
1637             $po->poRevisionDate();
1638              
1639             =head2 potCreationDate
1640              
1641             Returns a string containing the value of the header C<POT-Creation-Date>.
1642              
1643             $po->potCreationDate();
1644              
1645             =head2 projectIdVersion
1646              
1647             Returns a string containing the value of the header C<Project-Id-Version>.
1648              
1649             $po->projectIdVersion();
1650              
1651             =head2 reportBugsTo
1652              
1653             Returns a string containing the value of the header C<Report-Msgid-Bugs-To>.
1654              
1655             $po->reportBugsTo();
1656              
1657             =head2 textdomain
1658              
1659             $po->textdomain( $domain );
1660              
1661             Given a string representing a domain, such as C<com.example.api> and this will load (or reload) the C<.json> (if the L</use_json> option is enabled), C<.po> or C<.mo> file found in that order.
1662              
1663             Normally you do not need to call this directly; lookups such as L</gettext> will ensure the relevant domain is available.
1664              
1665             Internally, domain data is cached in a per-class repository (using L<Module::Generic::Global>) so that repeated lookups for the same domain and locale are efficient and thread-safe.
1666              
1667             =head2 use_json
1668              
1669             my $flag = $po->use_json; # 1
1670             $po->use_json(1);
1671              
1672             Takes a boolean and if set, L<Text::PO::Gettext> will use a json po data if it exists, otherwise it will use a C<.po> file or a C<.mo> file in that order of preference.
1673              
1674             =head2 _get_po
1675              
1676             Returns the L<Text::PO> object used.
1677              
1678             =head1 JAVASCRIPT COMPANION
1679              
1680             This distribution provides a JavaScript companion library (see the C<share> directory in the L<Text::PO> distribution) that can read JSON-based PO data generated from your C<.po> or C<.mo> files. This makes it straightforward to share the same localisation data between Perl and browser-side code.
1681              
1682             =head1 THREAD & PROCESS SAFETY
1683              
1684             L<Text::PO::Gettext> is designed to be fully thread-safe and process-safe, ensuring data integrity across Perl ithreads and mod_perl’s threaded Multi-Processing Modules (MPMs) such as Worker or Event. It combines system-level file locking (C<flock>) with Perl-level synchronisation via L<Module::Generic::Global> to provide robust, system-wide thread-safe, and process-safe file operations.
1685              
1686             =head1 AUTHOR
1687              
1688             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
1689              
1690             =head1 SEE ALSO
1691              
1692             L<Text::PO>, L<Text::PO::String>, L<Text::PO::Element>, L<Text::PO::MO>
1693              
1694             L<https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html>
1695              
1696             =head1 COPYRIGHT & LICENSE
1697              
1698             Copyright(c) 2021-2025 DEGUEST Pte. Ltd. DEGUEST Pte. Ltd.
1699              
1700             =cut