File Coverage

blib/lib/PGXN/Site/Locale.pm
Criterion Covered Total %
statement 66 67 98.5
branch 15 18 83.3
condition 1 3 33.3
subroutine 13 13 100.0
pod 4 4 100.0
total 99 105 94.2


line stmt bran cond sub pod time code
1             package PGXN::Site::Locale;
2              
3 1     1   68424 use 5.10.0;
  1         12  
4 1     1   7 use utf8;
  1         2  
  1         5  
5 1     1   569 use parent 'Locale::Maketext';
  1         311  
  1         4  
6 1     1   12420 use I18N::LangTags::Detect;
  1         2  
  1         24  
7 1     1   4 use File::Spec;
  1         2  
  1         27  
8 1     1   5 use Carp;
  1         1  
  1         951  
9             our $VERSION = v0.22.2;
10              
11             # Allow unknown phrases to just pass-through.
12             our %Lexicon = (
13             # _AUTO => 1,
14             listcomma => ',',
15             listand => 'and',
16             openquote => '“',
17             shutquote => '”',
18             in => 'in',
19             hometitle => 'PGXN: PostgreSQL Extension Network',
20             'PostgreSQL Extension Network' => 'PostgreSQL Extension Network',
21             'PGXN Gear' => 'PGXN Gear',
22             'Recent' => 'Recent',
23             'Recent Releases' => 'Recent Releases',
24             'About' => 'About',
25             'About PGXN' => 'About PGXN',
26             'PGXN Users' => 'PGXN Users',
27             'Recent' => 'Recent',
28             'User' => 'User',
29             'Users' => 'Users',
30             'Recent' => 'Recent',
31             'Recent Releases' => 'Recent Releases',
32             'Blog' => 'Blog',
33             'PGXN Blog' => 'PGXN Blog',
34             'FAQ' => 'FAQ',
35             'Frequently Asked Questions' => 'Frequently Asked Questions',
36             'Release It' => 'Release It',
37             'Release it on PGXN' => 'Release it on PGXN',
38             code => 'code',
39             design => 'design',
40             logo => 'logo',
41             'Go to [_1]' => 'Go to [_1]',
42             Mirroring => 'Mirroring',
43             'Mirroring PGXN' => 'Mirroring PGXN',
44             Feedback => 'Feedback',
45             Identity => 'Identity',
46             Extensions => 'Extensions',
47             Tags => 'Tags',
48             'Release Tags' => 'Release Tags',
49             Distributions => 'Distributions',
50             'PGXN Search' => 'PGXN Search',
51             pgxn_summary_paragraph => 'PGXN, the PostgreSQL Extension network, is a central distribution system for open-source PostgreSQL extension libraries.',
52             Founders => 'Founders',
53             Patrons => 'Patrons',
54             Benefactors => 'Benefactors',
55             Sponsors => 'Sponsors',
56             Advocates => 'Advocates',
57             Supporters => 'Supporters',
58             Boosters => 'Boosters',
59             'Donors' => 'Donors',
60             'See a longer list of recent releases.' => 'See a longer list of recent releases.',
61             'More Releases' => 'More Releases →',
62             'Not Found' => 'Not Found',
63             'Resource not found.' => 'Resource not found.',
64             'Resource Not Found' => 'Resource Not Found',
65             'Internal Server Error' => 'Internal Server Error',
66             'Internal server error.' => 'Internal server error.',
67             'Download' => 'Download',
68             'Download [_1] [_2]' => 'Download [_1] [_2]',
69             'Browse [_1] [_2]' => 'Browse [_1] [_2]',
70             'Alas, [_1] has yet to release a distribution.' => 'Alas, [_1] has yet to release a distribution.',
71             'This Release' => 'This Release',
72             'Date' => 'Date',
73             'Latest Stable' => 'Latest Stable',
74             'Latest Testing' => 'Latest Testing',
75             'Latest Unstable' => 'Latest Unstable',
76             'Other Releases' => 'Other Releases',
77             'Status' => 'Status',
78             'stable' => 'Stable',
79             'testing' => 'Testing',
80             'unstable' => 'Unstable',
81             'Abstract' => 'Abstract',
82             'Description' => 'Description',
83             'Maintainer' => 'Maintainer',
84             'Maintainers' => 'Maintainers',
85             'License' => 'License',
86             'Resources' => 'Resources',
87             'www' => 'www',
88             'bugs' => 'bugs',
89             'repo' => 'repo',
90             'Special Files' => 'Special Files',
91             'Tags' => 'Tags',
92             'Other Documentation' => 'Other Documentation',
93             'Released By' => 'Released By',
94             'README' => 'README',
95             'Documentation' => 'Documentation',
96             'Nickname' => 'Nickname',
97             'URL' => 'URL',
98             'Email' => 'Email',
99             'Mastodon' => 'Mastodon',
100             'Follow PGXN on Mastodon' => 'Follow PGXN on Mastodon',
101             'Twitter' => 'Twitter',
102             'Follow PGXN on Twitter' => 'Follow PGXN on Twitter',
103             'Browse' => 'Browse',
104             'Tag: [_1]' => 'Tag: “[_1]”',
105             'PGXN Search' => 'PGXN Search',
106             'In the [_1] distribution' => 'In the [_1] distribution',
107             'Released by [_1]' => 'Released by [_1]',
108             'Search matched no documents.' => 'Search matched no documents.',
109             'Previous results' => 'Previous results',
110             'Next results' => 'Next results',
111             '← Prev' => '← Prev',
112             'Next →' => 'Next →',
113             '[_1]-[_2] of [_3] found' => '[_1]-[_2] of [_3] found',
114             'No Releases Yet' => 'No Releases Yet',
115             'PGXN Meta Spec' => 'PGXN Meta Spec',
116             'Bad Request' => 'Bad Request',
117             'Bad request: Missing or invalid "[_1]" query parameter.' => 'Bad request: Missing or invalid “[_1]” query parameter.',
118             'Search Users' => 'Search Users',
119             'Or select a letter' => 'Or select a letter',
120             'Nicknames starting with "[_1]"' => 'User nicknames starting with “[_1]”',
121             'None found' => 'None found',
122             'Search all indexed extensions, distributions, users, and tags on the PostgreSQL Extension Network.' => 'Search all indexed extensions, distributions, users, and tags on the PostgreSQL Extension Network.',
123             'No user nicknames found starting with "[_1]"' => 'No user nicknames found starting with “[_1]”',
124             'Contact and extension release information for PGXN user "[_1]"' => 'Contact and extension release information for PGXN user “[_1]”',
125             'Search for tags on PostgreSQL extension releases on PGXN' => 'Search for tags on PostgreSQL extension releases on PGXN',
126             'Search for PostgreSQL Extension Network users' => 'Search for PostgreSQL Extension Network users',
127             'A list of PGXN extensions tagged "[_1]"' => 'A list of PGXN extensions tagged “[_1]”',
128             'PGXN [_1] search results for "[_2]"' => 'PGXN [_1] search results for “[_2]”',
129             'Submit feedback to PGXN or join the mail list' => 'Submit feedback to PGXN or join the mail list',
130             'Background on PGXN' => 'All about PGXN, what it’s for, what it contains, who made it, and why',
131             'donor description' => 'Many thanks to these fine organizations and people who contributed support to make the develpoment of PGXN possible',
132             'identity description' => 'All about the PGXN identity: who created it, its license, and downloadable assets',
133             'faq description' => 'Frequently asked questions about the PostgreSQL Extension Network',
134             'mirroring description' => 'Step-by-step instructions for mirroring PGXN on your own server',
135             'Recent PostgreSQL extension releases on PGXN' => 'Recent PostgreSQL extension releases on PGXN',
136             donors_intro => 'All the great folks who funded the inital development of PGXN will be listed in perpetuity here on the “Donors” page of PGXN.org. All donors are invited to the PGXN Launch Party at PGCon in May, 2011.',
137             );
138              
139             sub accept {
140 2     2 1 1364 shift->get_handle( I18N::LangTags::Detect->http_accept_langs(shift) );
141             }
142              
143             sub list {
144 6     6 1 3358 my ($lh, $items) = @_;
145 6 50       12 return unless @{ $items };
  6         15  
146 6 100       10 return $items->[0] if @{ $items } == 1;
  6         21  
147 4         5 my $last = pop @{ $items };
  4         9  
148 4         13 my $comma = $lh->maketext('listcomma');
149 4         137 my $ret = join "$comma ", @$items;
150 4 100       17 $ret .= $comma if @{ $items } > 1;
  4         13  
151 4         13 my $and = $lh->maketext('listand');
152 4         129 return "$ret $and $last";
153             }
154              
155             sub qlist {
156 6     6 1 3732 my ($lh, $items) = @_;
157 6 50       17 return unless @{ $items };
  6         18  
158 6         16 my $open = $lh->maketext('openquote');
159 6         185 my $shut = $lh->maketext('shutquote');
160 6 100       160 return $open . $items->[0] . $shut if @{ $items } == 1;
  6         25  
161 4         5 my $last = pop @{ $items };
  4         9  
162 4         8 my $comma = $lh->maketext('listcomma');
163 4         111 my $ret = $open . join("$shut$comma $open", @$items) . $shut;
164 4 100       7 $ret .= $comma if @{ $items } > 1;
  4         13  
165 4         10 my $and = $lh->maketext('listand');
166 4         118 return "$ret $and $open$last$shut";
167             }
168              
169             my %PATHS_FOR;
170              
171             sub DESTROY {
172 9     9   6157 delete $PATHS_FOR{ ref shift };
173             }
174              
175             sub from_file {
176 3     3 1 12 my ($self, $path) = (shift, shift);
177 3         7 my $class = ref $self;
178 3   33     31 my $file = $PATHS_FOR{$class}{$path} ||= _find_file($class, $path);
179 3 50       126 open my $fh, '<:utf8', $file or die "Cannot open $file: $!\n";
180 3         9 my $value = do { local $/; $self->_compile(<$fh>); };
  3         13  
  3         164  
181 3 100       1401 return ref $value eq 'CODE' ? $value->($self, @_) : ${ $value };
  2         47  
182             }
183              
184             sub _find_file {
185 3     3   6 my $class = shift;
186 3         11 my @path = split m{/}, shift;
187 3         19 (my $dir = __FILE__) =~ s{[.]pm$}{};
188 1     1   8 no strict 'refs';
  1         23  
  1         160  
189 3         6 foreach my $super ($class, @{$class . '::ISA'}, __PACKAGE__ . '::en') {
  3         16  
190 7         41 my $file = File::Spec->catfile($dir, $super->language_tag, @path);
191 7 100       309 return $file if -e $file;
192             }
193 0           croak "No file found for path " . join('/', @path);
194             }
195              
196             1;
197              
198             =encoding utf8
199              
200             =head1 Name
201              
202             PGXN::Site::Locale - Localization for PGXN::Site
203              
204             =head1 Synopsis
205              
206             use PGXN::Site::Locale;
207             my $mt = PGXN::Site::Locale->accept($env->{HTTP_ACCEPT_LANGUAGE});
208              
209             =head1 Description
210              
211             This class provides localization support for PGXN::Site. Each locale must
212             create a subclass named for the locale and put its translations in the
213             C<%Lexicon> hash. It is further designed to support easy creation of
214             a handle from an HTTP_ACCEPT_LANGUAGE header.
215              
216             =head1 Interface
217              
218             The interface inherits from L and adds the following
219             method.
220              
221             =head2 Constructor Methods
222              
223             =head3 C
224              
225             my $mt = PGXN::Site::Locale->accept($env->{HTTP_ACCEPT_LANGUAGE});
226              
227             Returns a PGXN::Site::Locale handle appropriate for the specified
228             argument, which must take the form of the HTTP_ACCEPT_LANGUAGE string
229             typically created in web server environments and specified in L
230             3282|https://tools.ietf.org/html/rfc3282>. The parsing of this header is
231             handled by L.
232              
233             =head2 Instance Methods
234              
235             =head3 C
236              
237             # "Missing these keys: foo, bar, and baz"
238             say $mt->maketext(
239             'Missing these keys: [list,_1])'
240             [qw(foo bar baz)],
241             );
242              
243             Formats a list of items. The list of items to be formatted should be passed as
244             an array reference. If there is only one item, it will be returned. If there
245             are two, they will be joined with " and ". If there are more, there will be a
246             comma-separated list with the final item joined on ", and ".
247              
248             Note that locales can control the localization of the comma and "and" via the
249             C and C entries in their C<%Lexicon>s.
250              
251             =head3 C
252              
253             # "Missing these keys: “foo”, “bar”, and “baz”
254             say $mt->maketext(
255             'Missing these keys: [qlist,_1]'
256             [qw(foo bar baz)],
257             );
258              
259             Like C but quotes each item in the list. Locales can specify the
260             quotation characters to be used via the C and C entries
261             in their C<%Lexicon>s.
262              
263             =head3 C
264              
265             my $text = $mt->from_file('foo/bar.html');
266             my $msg = $mt->from_file('feedback.html', 'pgxn@example.com');
267              
268             Returns the contents of a localized file. The file argument should be
269             specified with Unix semantics, regardless of operating system. Whereas
270             subclasses contain short strings that need translating, the files can contain
271             complete documents. As with C, the support the full range variable
272             substitution, such as C<[_1]> and friends.
273              
274             If a file doesn't exist for the current language, C will fall
275             back on the same file path for any of its parent classes. If none has the
276             file, it will fall back on the English file.
277              
278             Localized files are maintained in L format by translators
279             and converted to HTML at build time. The live in a subdirectory named for the
280             last part of a subclass's package name. For example, the
281             L class lives in F. Localized
282             files will live in F. So for the argument
283             C, the localized file will be
284             F, and the HTML file (created at build time)
285             will be F.
286              
287             =head1 Author
288              
289             David E. Wheeler
290              
291             =head1 Copyright and License
292              
293             Copyright (c) 2010-2021 David E. Wheeler.
294              
295             This module is free software; you can redistribute it and/or modify it under
296             the L.
297              
298             Permission to use, copy, modify, and distribute this software and its
299             documentation for any purpose, without fee, and without a written agreement is
300             hereby granted, provided that the above copyright notice and this paragraph
301             and the following two paragraphs appear in all copies.
302              
303             In no event shall David E. Wheeler be liable to any party for direct,
304             indirect, special, incidental, or consequential damages, including lost
305             profits, arising out of the use of this software and its documentation, even
306             if David E. Wheeler has been advised of the possibility of such damage.
307              
308             David E. Wheeler specifically disclaims any warranties, including, but not
309             limited to, the implied warranties of merchantability and fitness for a
310             particular purpose. The software provided hereunder is on an "as is" basis,
311             and David E. Wheeler has no obligations to provide maintenance, support,
312             updates, enhancements, or modifications.
313              
314             =cut