File Coverage

blib/lib/Ado/Plugin/I18n.pm
Criterion Covered Total %
statement 75 78 96.1
branch 22 26 84.6
condition 31 38 81.5
subroutine 10 10 100.0
pod 4 4 100.0
total 142 156 91.0


line stmt bran cond sub pod time code
1             package Ado::Plugin::I18n;
2 23     23   25782 use Mojo::Base 'Ado::Plugin';
  23         49  
  23         146  
3 23     23   17740 use I18N::LangTags qw(implicate_supers);
  23         57223  
  23         1933  
4 23     23   11974 use I18N::LangTags::Detect;
  23         26489  
  23         742  
5 23     23   10608 use Time::Seconds;
  23         33393  
  23         32274  
6              
7             #example.com/cz/foo/bar
8             sub routes {
9 25     25 1 56 my $self = shift;
10 25         118 my $config = $self->config;
11 25 100       135 return $$config{routes} if $$config{routes};
12 24 50       103 return [] unless $$config{language_from_route};
13 24         50 my $l = join '|', @{$$config{languages}};
  24         1636  
14             return $self->config->{routes} = [
15              
16             #Language prefixed front-end controllers
17             { route => '/:language',
18             params => {$$config{language_param} => qr/(?:$l)/},
19             via => [qw(GET OPTIONS)],
20              
21             #Ado::Control::Default::index()
22             to => 'default#index',
23             },
24             { route => '/:language/:controller',
25             via => [qw(GET OPTIONS)],
26             params => {$$config{language_param} => qr/(?:$l)/, controller => qr/[\w-]{3,}/},
27             to => {
28              
29             #Ado::Control::Default
30             controller => 'Default',
31             action => 'index'
32             }
33             },
34             { route => '/:language/:controller/:action',
35             via => [qw(GET POST OPTIONS)],
36             params => {
37             $$config{language_param} => qr/(?:$l)/,
38             controller => qr/[\w-]{3,}/,
39             action => qr/\w{3,}/
40             },
41             to => {
42              
43             #Ado::Control::Default
44             controller => 'Default',
45             action => 'index'
46             }
47             },
48             { route => '/:language/:controller/:action/:id',
49             via => [qw(GET PUT DELETE OPTIONS)],
50             params => {
51 24         2771 $$config{language_param} => qr/(?:$l)/,
52             controller => qr/[\w-]{3,}/,
53             action => qr/\w{3,}/,
54             id => qr/\d+/
55             },
56             to => {
57              
58             #Ado::Control::Default
59             controller => 'Default',
60             action => 'form'
61             }
62             },
63             ];
64             }
65              
66              
67             sub register {
68 25     25 1 2559 my ($self, $app, $config) = shift->initialise(@_);
69              
70             #Make sure we have all we need from config files.
71 25   100     369 $config->{default_language} ||= 'en';
72              
73             #Supported languages by this system
74 25   100     172 $config->{languages} ||= ['en', 'de', 'bg'];
75              
76             # Language will be guessed from one of these places in the order below.
77             # Specify/narrow your preferences in etc/plugins/i18n.conf.
78 25   100     155 $config->{language_from_route} //= 1;
79 25   100     137 $config->{language_from_host} //= 1;
80 25   100     223 $config->{language_from_param} //= 1;
81 25   100     110 $config->{language_from_cookie} //= 1;
82 25   100     112 $config->{language_from_headers} //= 1;
83 25   100     202 $config->{language_param} //= 'language';
84              
85             #Allow other namespaces too
86 25   100     189 $config->{namespace} ||= 'Ado::I18n';
87              
88 25         87 my $e = Mojo::Loader::load_class($config->{namespace});
89 25 50       487 $app->log->error(qq{Loading "$config->{namespace}" failed: $e}) if $e;
90              
91             # Defaults for $c->stash so templates without controllers can be used
92 25         202 $app->defaults(language => '', language_from => '');
93              
94             #Add helpers
95 25         693 $app->helper(language => \&language);
96 25         695 $app->helper(l => \&_maketext);
97              
98             #default routes including language placeholder.
99 25         352 $app->load_routes($self->routes);
100              
101             # Add hook around_action
102 25         237 $app->hook(around_action => \&around_action);
103              
104             #Add to classes used for finding templates in DATA sections
105 25         500 push @{$app->renderer->classes}, __PACKAGE__;
  25         274  
106              
107             #make plugin configuration available for later in the app
108 25         502 $app->config(__PACKAGE__, $config);
109 25         1133 return $self;
110             }
111              
112             #Mojolicious::around_action hook.
113             sub around_action {
114 106     106 1 1471175 my ($next, $c, $action, $last_step) = @_;
115 106         1218 $c->language();
116 106         426 return $next->();
117             }
118              
119             # Sets *once* and/or returns the current language - a controller attribute
120             sub language {
121 130     130 1 6061 my ($c, $language) = @_;
122 130         243 state $config = $c->app->config(__PACKAGE__);
123 130         399 state $l_param = $$config{language_param};
124 130         481 my $stash = $c->stash;
125              
126             #language('fr') should be used in very
127             #rare cases since it is called in around_action
128 130 100       1274 if ($language) {
129             $stash->{i18n} =
130 3         14 $$config{namespace}->get_handle($language, $c, $config);
131 3         19 $c->debug("explicitly set by developer.");
132 3         62 return $stash->{$l_param} = $language;
133             }
134              
135             #already set from route or called in an action as: $c->language()
136 127 100       501 if ($stash->{$l_param}) {
137             $stash->{i18n}
138 27   66     183 ||= $$config{namespace}->get_handle($stash->{$l_param}, $c, $config);
139 27         204 $c->debug("already set in \$stash->{$l_param}:");
140 27         706 return $stash->{$l_param};
141             }
142              
143             #bg.example.com
144 100 50 33     790 if ($$config{language_from_host}
145             && (my ($l) = $c->req->headers->host =~ /^(\w{2})\./))
146             {
147             $stash->{i18n} =
148 0         0 $$config{namespace}->get_handle($l, $c, $config);
149 0         0 $stash->{language_from} = 'host';
150 0         0 return $stash->{$l_param} = $l;
151             }
152              
153             #?language=de
154 100 100 100     3965 if ($$config{language_from_param}
      66        
155             && (my $l = ($c->param($l_param) // '')))
156             {
157 8         2487 $stash->{$l_param} = $l;
158 8         24 $stash->{language_from} = 'param';
159             }
160              
161 100 100 66     23296 if ( !$stash->{$l_param}
    100 66        
      66        
162             && $$config{language_from_cookie}
163             && ($language = $c->cookie($l_param)))
164             {
165 5         718 $c->cookie($l_param => $language, {expires => time + ONE_MONTH});
166 5         883 $stash->{$l_param} = $language;
167 5         11 $stash->{language_from} = 'cookie';
168             }
169              
170             #Accept-Language:"bg,fr;q=0.8,en-us;q=0.5,en;q=0.3"
171             elsif (!$stash->{$l_param} && $$config{language_from_headers}) {
172 87         10031 my @languages =
173             I18N::LangTags::implicate_supers(
174             I18N::LangTags::Detect->http_accept_langs($c->req->headers->accept_language));
175 87         4901 foreach my $l (@languages) {
176 2     4   11 $stash->{$l_param} = List::Util::first { $_ =~ /$l/ } @{$$config{languages}};
  4         43  
  2         12  
177 2 50       13 last if $stash->{$l_param};
178             }
179 87 100       342 $c->debug("language_from_headers:$stash->{$l_param}") if $stash->{$l_param};
180             }
181              
182             #default
183 100 100       553 $stash->{$l_param} = $$config{default_language} unless $stash->{$l_param};
184             $stash->{i18n} =
185 100         863 $$config{namespace}->get_handle($stash->{$l_param}, $c, $config);
186 100         350 return $stash->{$l_param};
187             }
188              
189             sub _maketext {
190 746     746   237950 my $stash = $_[0]->stash;
191 746 100       9045 return ref($stash->{i18n}) ? $stash->{i18n}->maketext($_[1], @_[2 .. $#_]) : $_[1];
192             }
193              
194             1;
195              
196             =pod
197              
198             =encoding utf8
199              
200             =head1 NAME
201              
202             Ado::Plugin::I18n - Internationalization and localization for Ado
203              
204             =head1 SYNOPSIS
205              
206             This plugin just works. Nothing special needs to be done.
207              
208             #Override the current language.
209             #you need to do this only in rare cases (like in an Ado::Command)
210             $c->language('bg');
211             #what is my language?
212             my $current_language = $c->language;
213              
214             =head1 DESCRIPTION
215              
216             L localizes your application and site.
217             It automatically detects the current UserAgent language preferences
218             and selects the best fit from the supported by the application languages.
219             The current language is detected and set in L hook.
220             Various methods for setting the language are supported.
221              
222             =head1 OPTIONS
223              
224             The following options can be set in C or in C.
225             You have to create first C so Ado can pick it up.
226             You can enable all supported methods to detect the language in your application.
227              
228             The methods which will be used to detect and set the current
229             language are as follows:
230              
231             1. language_from_route, eg. /bg/controller/action
232             2. language_from_host, eg. fr.example.com
233             3. language_from_param, eg. ?language=de
234             4. language_from_cookie, eg. Cookie: language=bg;
235             5. language_from_headers, eg. Accept-Language: bg,fr;q=0.8,en-us;q=0.5,en;q=0.3
236              
237             Just be careful not to try to set the language in one request using two different
238             methods eg. C.
239              
240              
241             =head2 default_language
242              
243             The default value is C. This language is used when Ado is not able to detect
244             the language using any of the methods enabled by the options below.
245             If you want to set a different language be sure to create a language class
246             in your languages namespace. See also L.
247              
248             =head2 language_from_route
249              
250             {
251             language_from_route => 1
252             ...
253             }
254              
255             This is the first option that will be checked if enabled.
256             The plugin prepares a default set of routes containing information
257             about the language.
258              
259             /:language GET,OPTIONS
260             /:language/:controller GET,OPTIONS
261             /:language/:controller/:action GET,POST,OPTIONS
262             /:language/:controller/:action/:id GET,PUT,DELETE,OPTIONS
263              
264             The language will be detected from current routes eg. C
265             and put into the stash. Default value is 1.
266              
267             =head2 language_from_host
268              
269             {
270             language_from_host => 1,
271             language_from_route => 1,
272             ...
273             }
274              
275             This is the second option that will be checked if enabled.
276             If you use languages as subdomains make sure to disable C
277             or do not construct routes containing languages (eg. C).
278             Default value is 1.
279              
280             =head2 language_from_param
281              
282             {
283              
284             language_from_param => 1,
285             language_from_host => 0,
286             language_from_route => 0,
287             ...
288             }
289              
290             This is the third option that will be checked if enabled and if the language is
291             not yet detected using some of the previous methods.
292             Make sure to not construct urls like C
293             or even C. The result is usually not what you want.
294             Default value is 1.
295              
296             =head2 language_from_cookie
297              
298             {
299              
300             language_from_cookie => 1,
301             language_from_param => 1,
302             language_from_host => 0,
303             language_from_route => 0,
304             ...
305             }
306              
307             This is the fourth option that will be checked if enabled and if the language is
308             not yet detected using some of the previous methods.
309             This option is most suitable for applications which expect to find a cookie
310             with name "language" and value one of the supported languages.
311             Default value is 1.
312              
313             =head2 language_from_headers
314              
315             {
316              
317             language_from_headers => 1
318             language_from_cookie => 1,
319             language_from_param => 1,
320             language_from_host => 0,
321             language_from_route => 0,
322             ...
323             }
324              
325             This is the fifth option that will be checked if enabled and if the language is
326             not yet detected using some of the previous methods.
327             It is best to keep this option enabled.
328             Default value is 1.
329              
330             =head2 language_param
331              
332             #language_param=> 'l'
333             current language is <%= $l %>
334             Cookie: l=bg;
335             #language_param=> 'lang'
336             current language is <%= $lang %>
337             Cookie: lang=bg;
338              
339             The name of the parameter(key) used in C, C
340             and C. this is also the key used in the C<$c-Estash>.
341             Default value is "language".
342              
343             =head2 namespace
344              
345             The namespace used for language classes.
346             Default value is L.
347             You rarely will want to change this.
348              
349             =head1 HELPERS
350              
351             L exports the following helpers for use in
352             L methods and templates.
353              
354             =head2 l
355              
356             Wrapper for L.
357              
358             $c->render(text => $c->l('hello', $c->user->name));
359             <%= l('hello', user->name) %>
360              
361             =head2 language
362              
363             Allows you to (re)set the current language. You should not need to use this helper!
364             It is called automatically in L hook.
365             Note however that if you render a template directly (without controller) you need to
366             call it in the template. See C for an example.
367              
368             % language('bg');
369              
370             =head1 TEMPLATES
371              
372             L contains one embedded template.
373              
374             =head2 partials/language_menu.html.ep
375              
376             Generates HTML for a language menu.
377             If you want to modify the template you can inflate all templates and do that.
378             A usage example can be found at L after starting ado.
379              
380             berov@u165:~/opt/public_dev/Ado$ bin/ado inflate
381             ...
382             [exist] /home/berov/opt/public_dev/Ado/templates/partials
383             [write] /home/berov/opt/public_dev/Ado/templates/partials/language_menu.html.ep
384              
385             #then choose the preferred way to switch languages...
386             %= include 'partials/language_menu'; # use default language_from => 'route'
387             %= include 'partials/language_menu', language_from => 'route';
388             %= include 'partials/language_menu', language_from => 'host';
389             %= include 'partials/language_menu', language_from => 'param';
390             %= include 'partials/language_menu', language_from => 'cookie';
391              
392              
393             =head1 METHODS
394              
395             L inherits all methods from
396             L and implements the following new ones.
397              
398              
399             =head2 register
400              
401             This method is called by C<$app-Eplugin>.
402             Registers the plugin in L application and merges internationalization
403             and localization configuration from C<$MOJO_HOME/etc/ado.conf> with settings
404             defined in C<$MOJO_HOME/etc/plugins/i18n.conf>. Authentication settings
405             defined in C will overwrite those defined in C.
406             Returns C<$self>.
407              
408             =head2 routes
409              
410             Returns a list of routes with C<:language> placeholder
411             defined in the plugin configuration. Called in L.
412             To create your own routes just create C and add them to it.
413             They will replace the default routes.
414              
415             #default routes including language placeholder.
416             $app->load_routes($self->routes);
417              
418             /:language GET,OPTIONS
419             /:language/:controller GET,OPTIONS
420             /:language/:controller/:action GET,POST,OPTIONS
421             /:language/:controller/:action/:id GET,PUT,DELETE,OPTIONS
422              
423             =head2 language
424              
425             This is the underlying subroutine used in C hook and c
426             helper.
427              
428             #Add helpers
429             $app->helper(language => \&language);
430              
431             =head2 around_action
432              
433             This method is passed as reference to be used as L.
434              
435             # Add hook around_action
436             $app->hook(around_action => \&around_action);
437              
438             =head1 TODO
439              
440             Create a table with message entries which will be loaded by this plugin.
441              
442             Create user interface to add/edit entries.
443              
444             =head1 SEE ALSO
445              
446             L, L, L,
447             L,
448             L, L
449              
450             =head1 SPONSORS
451              
452             The original author
453              
454             =head1 AUTHOR
455              
456             Красимир Беров (Krasimir Berov)
457              
458             =head1 COPYRIGHT AND LICENSE
459              
460             Copyright 2014 Красимир Беров (Krasimir Berov).
461              
462             This program is free software, you can redistribute it and/or
463             modify it under the terms of the
464             GNU Lesser General Public License v3 (LGPL-3.0).
465             You may copy, distribute and modify the software provided that
466             modifications are open source. However, software that includes
467             the license may release under a different license.
468              
469             See http://opensource.org/licenses/lgpl-3.0.html for more information.
470              
471             =cut
472              
473             __DATA__