File Coverage

blib/lib/PGXN/Site/Locale.pm
Criterion Covered Total %
statement 72 73 98.6
branch 15 18 83.3
condition 1 3 33.3
subroutine 15 15 100.0
pod 4 4 100.0
total 107 113 94.6


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