File Coverage

blib/lib/Mojolicious/Plugin/MultiLanguage.pm
Criterion Covered Total %
statement 15 127 11.8
branch 0 54 0.0
condition 0 30 0.0
subroutine 5 19 26.3
pod 1 1 100.0
total 21 231 9.0


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::MultiLanguage;
2 1     1   658 use Mojo::Base "Mojolicious::Plugin";
  1         2  
  1         10  
3              
4 1     1   1014 use Mojo::Collection 'c';
  1         2  
  1         50  
5 1     1   391 use HTTP::AcceptLanguage;
  1         2154  
  1         1841  
6              
7             our $VERSION = "0.01";
8             $VERSION = eval $VERSION;
9              
10             sub register {
11 0     0 1   my ($self, $app, $conf) = @_;
12              
13 0   0       $conf->{cookie} //= {path => "/"};
14 0   0       $conf->{languages} //= [qw/es fr de zh-tw/];
15 0   0       $conf->{api_prefix} //= ["/api"];
16              
17             state $langs_enabled = c(
18 0           'en', @{$conf->{languages}}
19 0     0     )->map(sub { lc $_ })->flatten->uniq;
  0            
20              
21             state $langs_available = c(
22             # English
23             {
24             code => 'en',
25             name => "English",
26             native => "English",
27             dir => 'ltr',
28             index2 => 1,
29             index3 => 1,
30             },
31              
32             # Spanish
33             {
34             code => 'es',
35             name => "Spanish",
36             native => "Español",
37             dir => 'ltr',
38             index2 => 2,
39             index3 => 2,
40             },
41              
42             # German
43             {
44             code => 'de',
45             name => "German",
46             native => "Deutsch",
47             dir => 'ltr',
48             index2 => 3,
49             index3 => 3,
50             },
51              
52             # French
53             {
54             code => 'fr',
55             name => "French",
56             native => "Français",
57             dir => 'ltr',
58             index2 => 4,
59             index3 => 4,
60             },
61              
62             # Portuguese
63             {
64             code => 'pt-br',
65             name => "Portuguese",
66             native => "Português",
67             dir => 'ltr',
68             index2 => 5,
69             index3 => 5,
70             },
71              
72             # Italian
73             {
74             code => 'it',
75             name => "Italian",
76             native => "italiano",
77             dir => 'ltr',
78             index2 => 6,
79             index3 => 6,
80             },
81              
82             # Polish
83             {
84             code => 'pl',
85             name => "Polish",
86             native => "Polskie",
87             dir => 'ltr',
88             index2 => 7,
89             index3 => 7,
90             },
91              
92             # Russian
93             {
94             code => 'ru',
95             name => "Russian",
96             native => "Русский",
97             dir => 'ltr',
98             index2 => 8,
99             index3 => 8,
100             },
101              
102             # Ukrainian
103             {
104             code => 'uk',
105             name => "Ukrainian",
106             native => "Українська",
107             dir => 'ltr',
108             index2 => 9,
109             index3 => 9,
110             },
111              
112             # Finnish
113             {
114             code => 'fi',
115             name => "Finnish",
116             native => "Finnish",
117             dir => 'ltr',
118             index2 => 10,
119             index3 => 10,
120             },
121              
122             # Greek
123             {
124             code => 'el',
125             name => "Greek",
126             native => "Ελληνικά",
127             dir => 'ltr',
128             index2 => 11,
129             index3 => 11,
130             },
131              
132             # Turkish
133             {
134             code => 'tr',
135             name => "Turkish",
136             native => "Türk",
137             dir => 'ltr',
138             index2 => 12,
139             index3 => 12,
140             },
141              
142             # Arabic
143             {
144             code => 'ar',
145             name => "Arabic",
146             native => "العربية",
147             dir => 'rtl',
148             index2 => 13,
149             index3 => 13,
150             },
151              
152             # Farsi
153             {
154             code => 'fa',
155             name => "Farsi",
156             native => "हिंदी",
157             dir => 'rtl',
158             index2 => 14,
159             index3 => 14,
160             },
161              
162             # Hindi
163             {
164             code => 'hi',
165             name => "Hindi",
166             native => "हिंदी",
167             dir => 'ltr',
168             index2 => 15,
169             index3 => 15,
170             },
171              
172             # Chinese
173             {
174             code => 'zh-cn',
175             name => "Chinese (Simplified)",
176             native => "中国",
177             dir => 'ltr',
178             index2 => 16,
179             index3 => 16,
180             },
181              
182             {
183             code => 'zh-tw',
184             name => "Chinese (Traditional)",
185             native => "中国",
186             dir => 'ltr',
187             index2 => 17,
188             index3 => 17,
189             },
190              
191             # Japanese
192             {
193             code => 'ja',
194             name => "Japanese",
195             native => "日本",
196             dir => 'ltr',
197             index2 => 18,
198             index3 => 18,
199             },
200              
201             # Korean
202             {
203             code => 'ko',
204             name => "Korean",
205             native => "日本",
206             dir => 'ltr',
207             index2 => 19,
208             index3 => 19,
209             }
210 0     0     )->each(sub { $_->{index1} = 1 });
  0            
211              
212             # Default language
213 0           my $english = $langs_available->first;
214              
215             # Lookup language
216             my $lang_lookup = sub {
217 0     0     my ($code) = @_;
218              
219 0 0         $langs_available->grep(sub { lc $code eq $_->{code} })->first
  0            
220             or die "Language code '$code' does not exists!";
221 0           };
222              
223             # Enabled languages
224             $app->attr(languages => sub {
225 0     0     $langs_enabled->map(sub { $lang_lookup->($_) });
  0            
226 0           });
227              
228             # Active languages codes
229             $app->attr(langs => sub {
230 0     0     $app->languages->map( sub { $_->{code} });
  0            
231 0           });
232              
233             my $lang_exists = sub {
234 0     0     my ($code) = @_;
235              
236 0 0 0       return 0 unless $code and $code =~ /^[a-z]{2}(-[a-z]{2})?$/;
237 0           $app->languages->grep(sub { $code eq $_->{code} })->size;
  0            
238 0           };
239              
240             # Parse Accept-Language header
241             $app->helper(accept_language => sub {
242 0     0     my ($c) = @_;
243              
244 0           my $header = $c->req->headers->accept_language;
245 0           HTTP::AcceptLanguage->new($header)->match(@{$app->langs});
  0            
246 0           });
247              
248             # Detect language for site via url, cookie or headers
249             my $detect_site = sub {
250 0     0     my ($c, $path) = @_;
251              
252 0   0       my $part = $path->parts->[0] // '';
253 0           my @flags = (0, $english->{code}, 0, "/");
254              
255 0 0         unless ($part) {
    0          
    0          
256 0           my $cookie = $c->cookie('lang');
257              
258 0 0         unless ($cookie) {
    0          
    0          
259 0           my $accept = $c->accept_language;
260              
261 0 0         unless ($accept) {
    0          
    0          
262 0           $app->log->debug("Unknown accept-language");
263             }
264              
265 0           elsif ($accept eq $english->{code}) {
266 0           @flags[1] = ($accept);
267             }
268              
269 0           elsif ($lang_exists->($accept)) {
270 0           @flags[1, 2, 3] = ($accept, 1, "/$accept");
271             }
272              
273             else {
274 0           $app->log->warn("Wrong accept-language: '$accept'");
275             }
276             }
277              
278 0           elsif ($cookie eq $english->{code}) {
279 0           @flags[1] = ($cookie);
280             }
281              
282 0           elsif ($lang_exists->($cookie)) {
283 0           @flags[1, 2, 3] = ($cookie, 1, "/$cookie");
284             }
285              
286             else {
287 0           $app->log->warn("Wrong cookie-language: '$cookie'");
288             }
289             }
290              
291 0           elsif ($part eq $english->{code}) {
292 0           @flags[0, 1, 2, 3] = (1, $part, 1, $path);
293             }
294              
295 0           elsif ($lang_exists->($part)) {
296 0           @flags[0, 1, 2, 3] = (1, $part, 0, $path);
297             }
298              
299             else {
300 0           $app->log->debug("No language detected");
301             }
302              
303 0 0         if ($flags[0]) {
304 0           shift @{$path->parts};
  0            
305 0           $path->trailing_slash(0);
306             }
307              
308 0           my $language = $lang_lookup->($flags[1]);
309 0           $c->cookie(lang => $language->{code}, $conf->{cookie});
310              
311 0 0 0       $c->redirect_to($flags[3]) and return undef if $flags[2];
312              
313 0           $app->log->debug("Detect site language '$language->{code}'");
314              
315 0           return $language;
316 0           };
317              
318             # Detetect language for api via headers only
319             my $detect_api = sub {
320 0     0     my ($c, $path) = @_;
321              
322 0 0         return $english if $c->req->method eq 'OPTIONS';
323              
324 0           my $accept = $c->accept_language;
325              
326 0 0         my $language = $lang_exists->($accept)
327             ? $lang_lookup->($accept) : $english;
328              
329 0           $app->log->debug("Detect API language '$language->{code}'");
330              
331 0           return $language;
332 0           };
333              
334             $app->hook(before_dispatch => sub {
335 0     0     my ($c) = @_;
336              
337 0 0         return if $c->res->code;
338              
339 0           my $path = $c->req->url->path;
340 0           my $is_api = grep { $path->contains($_) } @{$conf->{api_prefix}};
  0            
  0            
341              
342 0 0         return unless my $language = $is_api
    0          
343             ? $detect_api->($c, $path) : $detect_site->($c, $path);
344              
345 0           $c->stash(language => $language);
346 0           });
347              
348             $app->hook(after_render => sub {
349 0     0     my ($c) = @_;
350              
351 0 0         return unless my $language = $c->stash('language');
352              
353 0           my $h = $c->res->headers;
354 0           $h->append('Vary' => "Accept-Language");
355 0           $h->content_language($language->{code});
356 0           });
357              
358             # Complete languages collection
359             $app->helper(languages => sub {
360 0     0     my ($c) = @_;
361              
362 0   0       my $language = $c->stash('language') // $english;
363              
364             $app->languages->each(sub {
365 0 0         $_->{active} = $_->{code} eq $language->{code} ? 1 : 0;
366 0           });
367 0           });
368              
369             # Reimplement 'url_for' helper
370 0           my $mojo_url_for = *Mojolicious::Controller::url_for{CODE};
371              
372             my $lang_url_for = sub {
373 0     0     my ($c, @args) = @_;
374              
375 0           my $url = $c->$mojo_url_for(@args);
376              
377 0 0         return $url if $url->is_abs;
378              
379 0 0 0       shift @args if @args % 2 && !ref $args[0] or @args > 1 && ref $args[-1];
      0        
      0        
380 0 0         my %params = @args == 1 ? %{$args[0]} : @_;
  0            
381              
382 0 0         return $url unless my $language = $c->stash('language');
383 0   0       my $code = $params{lang} // $language->{code};
384              
385 0 0         return $url if $code eq $english->{code};
386              
387 0   0       my $path = $url->path // [];
388              
389 0 0         unless ($path->[0]) {
390 0           $path->parts([$code]);
391             }
392              
393             else {
394             my $exists = $c->languages->grep(sub {
395             $path->contains(sprintf "/%s", $_->{code})
396 0           })->size;
  0            
397              
398 0 0         unshift @{$path->parts}, $code unless $exists;
  0            
399             }
400              
401 0           return $url;
402 0           };
403              
404             {
405 1     1   8 no strict 'refs';
  1         2  
  1         35  
  0            
406 1     1   5 no warnings 'redefine';
  1         1  
  1         85  
407              
408 0           *Mojolicious::Controller::url_for = $lang_url_for;
409             }
410             }
411              
412             1;
413              
414             __END__