File Coverage

blib/lib/Catalyst/Plugin/I18N/PathPrefix.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::I18N::PathPrefix;
2              
3 1     1   141837 use 5.008;
  1         4  
  1         44  
4              
5 1     1   516 use Moose::Role;
  0            
  0            
6             use namespace::autoclean;
7              
8             requires
9             # from Catalyst
10             'config', 'prepare_path', 'req', 'uri_for', 'log',
11             # from Catalyst::Plugin::I18N
12             'languages', 'loc';
13              
14             use List::Util qw(first);
15             use Scope::Guard;
16             use I18N::LangTags::List;
17              
18             =head1 NAME
19              
20             Catalyst::Plugin::I18N::PathPrefix - Language prefix in the request path
21              
22             =head1 VERSION
23              
24             Version 0.07
25              
26             =cut
27              
28             our $VERSION = '0.07';
29              
30              
31             =head1 SYNOPSIS
32              
33             # in MyApp.pm
34             use Catalyst qw/
35             I18N I18N::PathPrefix
36             /;
37             __PACKAGE__->config('Plugin::I18N::PathPrefix' => {
38             valid_languages => [qw/en de fr/],
39             fallback_language => 'en',
40             language_independent_paths => qr{
41             ^( votes/ | captcha/numeric/ )
42             }x,
43             });
44             __PACKAGE__->setup;
45              
46             # now the language is selected based on requests paths:
47             #
48             # http://www.example.com/en/foo/bar -> sets $c->language to 'en',
49             # dispatcher sees /foo/bar
50             #
51             # http://www.example.com/de/foo/bar -> sets $c->language to 'de',
52             # dispatcher sees /foo/bar
53             #
54             # http://www.example.com/fr/foo/bar -> sets $c->language to 'fr',
55             # dispatcher sees /foo/bar
56             #
57             # http://www.example.com/foo/bar -> sets $c->language from
58             # Accept-Language header,
59             # dispatcher sees /foo/bar
60              
61             # in a controller
62             sub language_switch : Private
63             {
64             # the template will display the language switch
65             $c->stash('language_switch' => $c->language_switch_options);
66             }
67              
68             =head1 DESCRIPTION
69              
70             This module allows you to put the language selector as a prefix to the path part of
71             the request URI without requiring any modifications to the controllers (like
72             restructuring all the controllers to chain from a common base controller).
73              
74             (Internally it strips the language code from C<< $c->req->path >> and appends
75             it to C<< $c->req->base >> so that the invariant C<< $c->req->uri eq
76             $c->req->base . $c->req->path >> still remains valid, but the dispatcher does
77             not see the language code - it uses C<< $c->req->path >> only.)
78              
79             Throughout this document 'language code' means ISO 639-1 2-letter language
80             codes, case insensitively (eg. 'en', 'de', 'it', 'EN'), just like
81             L<I18N::LangTags> supports them.
82              
83             Note: You have to load L<Catalyst::Plugin::I18N> if you load this plugin.
84              
85             Note: HTTP already have a standard way (ie. Accept-Language header) to allow
86             the user specify the language (s)he prefers the page to be delivered in.
87             Unfortunately users often don't set it properly, but more importantly Googlebot
88             does not really support it (but requires that you always serve documents of the
89             same language on the same URI). So if you want a SEO-optimized multi-lingual
90             site, you have to have different (sub)domains for the different languages, or
91             resort to putting the language selector into the URL.
92              
93             =head1 CONFIGURATION
94              
95             You can use these configuration options under the C<'Plugin::I18N::PathPrefix'>
96             key:
97              
98             =head2 valid_languages
99              
100             valid_languages => \@language_codes
101              
102             The language codes that are accepted as path prefix.
103              
104             =head2 fallback_language
105              
106             fallback_language => $language_code
107              
108             The fallback language code used if the URL contains no language prefix and
109             L<Catalyst::Plugin::I18N> cannot auto-detect the preferred language from the
110             C<Accept-Language> header or none of the detected languages are found in
111             L</valid_languages>.
112              
113             =head2 language_independent_paths
114              
115             language_independent_paths => $regex
116              
117             If the URI path is matched by C<$regex>, do not add language prefix and ignore
118             if there's one (and pretend as if the URI did not contain any language prefix,
119             ie. rewrite C<< $c->req->uri >>, C<< $c->req->base >> and C<< $c->req->path >>
120             to remove the prefix from them).
121              
122             Use a regex that matches all your paths that return language independent
123             information.
124              
125             If you don't set this config option or you set it to an undefined value, no
126             paths will be handled as language independent ones.
127              
128             =head2 debug
129              
130             debug => $boolean
131              
132             If set to a true value, L</prepare_path_prefix> logs its actions (using C<<
133             $c->log->debug(...) >>).
134              
135             =head1 METHODS
136              
137             =cut
138              
139             =head2 setup_finalize
140              
141             Overridden (wrapped with an an C<after> modifier) from
142             L<Catalyst/setup_finalize>.
143              
144             Sets up the package configuration.
145              
146             =cut
147              
148             after setup_finalize => sub {
149             my ($c) = (shift, @_);
150              
151             my $config = $c->config->{'Plugin::I18N::PathPrefix'};
152              
153             $config->{fallback_language} = lc $config->{fallback_language};
154              
155             my @valid_language_codes = map { lc $_ }
156             @{ $config->{valid_languages} };
157              
158             # fill the hash for quick lookups
159             @{ $config->{_valid_language_codes}}{ @valid_language_codes } = ();
160              
161             if (!defined $config->{language_independent_paths}) {
162             $config->{language_independent_paths} = qr/(?!)/; # never matches anything
163             }
164             };
165              
166             =head2 prepare_path
167              
168             Overridden (wrapped with an an C<after> modifier) from
169             L<Catalyst/prepare_path>.
170              
171             Calls C<< $c->prepare_path_prefix >> after the original method.
172              
173             =cut
174              
175             after prepare_path => sub {
176             my ($c) = (shift, @_);
177              
178             $c->prepare_path_prefix;
179             };
180              
181             =head2 prepare_path_prefix
182              
183             $c->prepare_path_prefix()
184              
185             Returns: N/A
186              
187             If C<< $c->req->path >> is matched by the L</language_independent_paths>
188             configuration option then calls C<< $c->set_languages_from_language_prefix >>
189             with the value of the L</fallback_language> configuration option and
190             returns.
191              
192             Otherwise, if C<< $c->req->path >> starts with a language code listed in the
193             L</valid_languages> configuration option, then splits language prefix from C<<
194             $c->req->path >> then appends it to C<< $c->req->base >> and calls C<<
195             $c->set_languages_from_language_prefix >> with this language prefix.
196              
197             Otherwise, it tries to select an appropriate language code:
198              
199             =over
200              
201             =item *
202              
203             It picks the first language code C<< $c->languages >> that is also present in
204             the L</valid_languages> configuration option.
205              
206             =item *
207              
208             If no such language code, uses the value of the L</fallback_language>
209             configuration option.
210              
211             =back
212              
213             Then appends this language code to C<< $c->req->base >> and the path part of
214             C<< $c->req->uri >>, finally calls C<< $c->set_languages_from_language_prefix >>
215             with that language code.
216              
217             =cut
218              
219             sub prepare_path_prefix
220             {
221             my ($c) = (shift, @_);
222              
223             my $config = $c->config->{'Plugin::I18N::PathPrefix'};
224              
225             my $language_code = $config->{fallback_language};
226              
227             my $valid_language_codes = $config->{_valid_language_codes};
228              
229             my $req_path = $c->req->path;
230              
231             if ($req_path !~ $config->{language_independent_paths}) {
232             my ($prefix, $path) = split m{/}, $req_path, 2;
233             $prefix = lc $prefix if defined $prefix;
234             $path = '' if !defined $path;
235              
236             if (defined $prefix && exists $valid_language_codes->{$prefix}) {
237             $language_code = $prefix;
238              
239             $c->_language_prefix_debug("found language prefix '$language_code' "
240             . "in path '$req_path'");
241              
242             # can be a language independent path with surplus language prefix
243             if ($path =~ $config->{language_independent_paths}) {
244             $c->_language_prefix_debug("path '$path' is language independent");
245              
246             # bust the language prefix completely
247             $c->req->uri->path($path);
248              
249             $language_code = $config->{fallback_language};
250             }
251             else {
252             # replace the language prefix with the known lowercase one in $c->req->uri
253             $c->req->uri->path($language_code . '/' . $path);
254              
255             # since $c->req->path returns such a string that satisfies
256             # << $c->req->uri->path eq $c->req->base->path . $c->req->path >>
257             # this strips the language code prefix from $c->req->path
258             my $req_base = $c->req->base;
259             $req_base->path($req_base->path . $language_code . '/');
260             }
261             }
262             else {
263             my $detected_language_code =
264             first { exists $valid_language_codes->{$_} }
265             map { lc $_ }
266             @{ $c->languages };
267              
268             $c->_language_prefix_debug("detected language: "
269             . ($detected_language_code ? "'$detected_language_code'" : "N/A"));
270              
271             $language_code = $detected_language_code if $detected_language_code;
272              
273             # fake that the request path already contained the language code prefix
274             my $req_uri = $c->req->uri;
275             $req_uri->path($language_code . $req_uri->path);
276              
277             # so that it strips the language code prefix from $c->req->path
278             my $req_base = $c->req->base;
279             $req_base->path($req_base->path . $language_code . '/');
280              
281             $c->_language_prefix_debug("set language prefix to '$language_code'");
282             }
283              
284             $c->req->_clear_path;
285             }
286             else {
287             $c->_language_prefix_debug("path '$req_path' is language independent");
288             }
289              
290             $c->set_languages_from_language_prefix($language_code);
291             }
292              
293              
294             =head2 set_languages_from_language_prefix
295              
296             $c->set_languages_from_language_prefix($language_code)
297              
298             Returns: N/A
299              
300             Sets C<< $c->languages >> to C<$language_code>.
301              
302             Called from both L</prepare_path_prefix> and L</switch_language> (ie.
303             always called when C<< $c->languages >> is set by this module).
304              
305             You can wrap this method (using eg. the L<Moose/after> method modifier) so you
306             can store the language code into the stash if you like:
307              
308             after prepare_path_prefix => sub {
309             my $c = shift;
310              
311             $c->stash('language' => $c->language);
312             };
313              
314             =cut
315              
316             sub set_languages_from_language_prefix
317             {
318             my ($c, $language_code) = (shift, @_);
319              
320             $language_code = lc $language_code;
321              
322             $c->languages([$language_code]);
323             }
324              
325              
326             =head2 uri_for_in_language
327              
328             $c->uri_for_in_language($language_code => @uri_for_args)
329              
330             Returns: C<$uri_object>
331              
332             The same as L<Catalyst/uri_for> but returns the URI with the C<$language_code>
333             path prefix (independently of what the current language is).
334              
335             Internally this method temporarily sets the paths in C<< $c->req >>, calls
336             L<Catalyst/uri_for> then resets the paths. Ineffective, but you usually call it
337             very infrequently.
338              
339             Note: You should not call this method to generate language-independent paths,
340             as it will generate invalid URLs currently (ie. the language independent path
341             prefixed with the language prefix).
342              
343             Note: This module intentionally does not override L<Catalyst/uri_for> but
344             provides this method instead: L<Catalyst/uri_for> is usually called many times
345             per request, and most of the cases you want it to use the current language; not
346             overriding it can be a significant performance saving. YMMV.
347              
348             =cut
349              
350             sub uri_for_in_language
351             {
352             my ($c, $language_code, @uri_for_args) = (shift, @_);
353              
354             $language_code = lc $language_code;
355              
356             my $scope_guard = $c->_set_language_prefix_temporarily($language_code);
357              
358             return $c->uri_for(@uri_for_args);
359             }
360              
361              
362             =head2 switch_language
363              
364             $c->switch_language($language_code)
365              
366             Returns: N/A
367              
368             Changes C<< $c->req->base >> to end with C<$language_code> and calls C<<
369             $c->set_languages_from_language_prefix >> with C<$language_code>.
370              
371             Useful if you want to switch the language later in the request processing (eg.
372             from a request parameter, from the session or from the user object).
373              
374             =cut
375              
376             sub switch_language
377             {
378             my ($c, $language_code) = (shift, @_);
379              
380             $language_code = lc $language_code;
381              
382             $c->_set_language_prefix($language_code);
383              
384             $c->set_languages_from_language_prefix($language_code);
385             }
386              
387              
388             =head2 language_switch_options
389              
390             $c->language_switch_options()
391              
392             Returns: C<< { $language_code => { name => $language_name, uri => $uri }, ... } >>
393              
394             Returns a data structure that contains all the necessary data (language code,
395             name, URL of the same page) for displaying a language switch widget on the
396             page.
397              
398             The data structure is a hashref with one key for each valid language code (see
399             the L</valid_languages> config option) (in all-lowercase format) and the value
400             is a hashref that contains the following key-value pairs:
401              
402             =over
403              
404             =item name
405              
406             The localized (translated) name of the language. (The actual msgid used in C<<
407             $c->loc() >> is the English name of the language, returned by
408             L<I18N::LangTags::List/name>.)
409              
410             =item url
411              
412             The URL of the equivalent of the current page in that language (ie. the
413             language prefix replaced).
414              
415             =back
416              
417             You can find an example TT2 HTML template for the language switch included in
418             the distribution.
419              
420             =cut
421              
422             sub language_switch_options
423             {
424             my ($c) = (shift, @_);
425              
426             return {
427             map {
428             $_ => {
429             name => $c->loc(I18N::LangTags::List::name($_)),
430             uri => $c->uri_for_in_language($_ => '/' . $c->req->path, $c->req->params),
431             }
432             } map { lc $_ }
433             @{ $c->config->{'Plugin::I18N::PathPrefix'}->{valid_languages} }
434             };
435             }
436              
437              
438             =begin internal
439              
440             $c->_set_language_prefix($language_code)
441              
442             Sets the language to C<$language_code>: Mangles C<< $c->req->uri >> and C<<
443             $c->req->base >>.
444              
445             =end internal
446              
447             =cut
448              
449             sub _set_language_prefix
450             {
451             my ($c, $language_code) = (shift, @_);
452              
453             if ($c->req->path !~
454             $c->config->{'Plugin::I18N::PathPrefix'}->{language_independent_paths}) {
455             my ($actual_base_path) = $c->req->base->path =~ m{ ^ / [^/]+ (.*) $ }x;
456             $c->req->base->path($language_code . $actual_base_path);
457              
458             my ($actual_uri_path) = $c->req->uri->path =~ m{ ^ / [^/]+ (.*) $ }x;
459             $c->req->uri->path($language_code . $actual_uri_path);
460              
461             $c->req->_clear_path;
462             }
463             }
464              
465              
466             =begin internal
467              
468             my $scope_guard = $c->_set_language_prefix_temporarily($language_code)
469              
470             Sets the language prefix temporarily (does the same as L</_set_language_prefix>
471             but returns a L<Scope::Guard> instance that resets the these on destruction).
472              
473             =end internal
474              
475             =cut
476              
477             sub _set_language_prefix_temporarily
478             {
479             my ($c, $language_code) = (shift, @_);
480              
481             my $old_req_uri_path = $c->req->uri->path;
482             my $old_req_base_path = $c->req->base->path;
483              
484             my $scope_guard = Scope::Guard->new(sub {
485             $c->req->uri->path($old_req_uri_path);
486             $c->req->base->path($old_req_base_path);
487             });
488              
489             $c->_set_language_prefix($language_code);
490              
491             return $scope_guard;
492             }
493              
494              
495             =begin internal
496              
497             $c->_language_prefix_debug($message)
498              
499             Logs C<$message> using C<< $c->log->debug("Plugin::I18N::PathPrefix: $message") >> if the
500             L</debug> config option is true.
501              
502             =end internal
503              
504             =cut
505              
506             sub _language_prefix_debug
507             {
508             my ($c, $message) = (shift, @_);
509              
510             $c->log->debug("Plugin::I18N::PathPrefix: $message")
511             if $c->config->{'Plugin::I18N::PathPrefix'}->{debug};
512             }
513              
514              
515             =head1 SEE ALSO
516              
517             L<Catalyst::Plugin::I18N>, L<Catalyst::TraitFor::Request::PerLanguageDomains>
518              
519             =head1 AUTHOR
520              
521             Norbert Buchmuller, C<< <norbi at nix.hu> >>
522              
523             =head1 TODO
524              
525             =over
526              
527             =item make L</uri_for_in_language> work on language-independent URIs
528              
529             =item support locales instead of language codes
530              
531             =back
532              
533             =head1 BUGS
534              
535             Please report any bugs or feature requests to
536             C<bug-catalyst-plugin-i18n-pathprefix at rt.cpan.org>, or through the web
537             interface at
538             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Catalyst-Plugin-I18N-PathPrefix>.
539             I will be notified, and then you'll automatically be notified of progress on
540             your bug as I make changes.
541              
542             =head1 SUPPORT
543              
544             You can find documentation for this module with the perldoc command.
545              
546             perldoc Catalyst::Plugin::I18N::PathPrefix
547              
548             You can also look for information at:
549              
550             =over 4
551              
552             =item * RT: CPAN's request tracker
553              
554             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Catalyst-Plugin-I18N-PathPrefix>
555              
556             =item * AnnoCPAN: Annotated CPAN documentation
557              
558             L<http://annocpan.org/dist/Catalyst-Plugin-I18N-PathPrefix>
559              
560             =item * CPAN Ratings
561              
562             L<http://cpanratings.perl.org/d/Catalyst-Plugin-I18N-PathPrefix>
563              
564             =item * Search CPAN
565              
566             L<http://search.cpan.org/dist/Catalyst-Plugin-I18N-PathPrefix/>
567              
568             =back
569              
570             =head1 ACKNOWLEDGEMENTS
571              
572             Thanks for Larry Leszczynski for the idea of appending the language prefix to
573             C<< $c->req->base >> after it's split off of C<< $c->req->path >>
574             (L<http://dev.catalystframework.org/wiki/wikicookbook/urlpathprefixing>).
575              
576             Thanks for Tomas (t0m) Doran <bobtfish@bobtfish.net> for the code reviews,
577             improvement ideas and mentoring in general.
578              
579             =head1 COPYRIGHT & LICENSE
580              
581             Copyright 2010 Norbert Buchmuller, all rights reserved.
582              
583             This program is free software; you can redistribute it and/or modify it
584             under the same terms as Perl itself.
585              
586             =cut
587              
588             1; # End of Catalyst::Plugin::I18N::PathPrefix