bug 3161: don't display blank language chooser
[koha.git] / C4 / Languages.pm
1 package C4::Languages;
2
3 # Copyright 2006 (C) LibLime
4 # Joshua Ferraro <jmf@liblime.com>
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it under the
9 # terms of the GNU General Public License as published by the Free Software
10 # Foundation; either version 2 of the License, or (at your option) any later
11 # version.
12 #
13 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
14 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License along with
18 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
19 # Suite 330, Boston, MA  02111-1307 USA
20
21
22 use strict; 
23 #use warnings;   #FIXME: turn off warnings before release
24 use Carp;
25 use C4::Context;
26 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
27
28 BEGIN {
29     $VERSION = 3.00;
30     require Exporter;
31     @ISA    = qw(Exporter);
32     @EXPORT = qw(
33         &getFrameworkLanguages
34         &getTranslatedLanguages
35         &getAllLanguages
36     );
37     @EXPORT_OK = qw(getFrameworkLanguages getTranslatedLanguages getAllLanguages get_bidi regex_lang_subtags language_get_description accept_language);
38     $DEBUG = 0;
39 }
40
41 =head1 NAME
42
43 C4::Languages - Perl Module containing language list functions for Koha 
44
45 =head1 SYNOPSIS
46
47 use C4::Languages;
48
49 =head1 DESCRIPTION
50
51 =head1 FUNCTIONS
52
53 =head2 getFrameworkLanguages
54
55 Returns a reference to an array of hashes:
56
57  my $languages = getFrameworkLanguages();
58  for my $language(@$languages) {
59     print "$language->{language_code}\n"; # language code in iso 639-2
60     print "$language->{language_name}\n"; # language name in native script
61     print "$language->{language_locale_name}\n"; # language name in current locale
62  }
63
64 =cut
65
66 sub getFrameworkLanguages {
67     # get a hash with all language codes, names, and locale names
68     my $all_languages = getAllLanguages();
69     my @languages;
70     
71     # find the available directory names
72     my $dir=C4::Context->config('intranetdir')."/installer/data/";
73     opendir (MYDIR,$dir);
74     my @listdir= grep { !/^\.|CVS/ && -d "$dir/$_"} readdir(MYDIR);    
75     closedir MYDIR;
76
77     # pull out all data for the dir names that exist
78     for my $dirname (@listdir) {
79         for my $language_set (@$all_languages) {
80
81             if ($dirname eq $language_set->{language_code}) {
82                 push @languages, {
83                     'language_code'=>$dirname, 
84                     'language_description'=>$language_set->{language_description}, 
85                     'native_descrition'=>$language_set->{language_native_description} }
86             }
87         }
88     }
89     return \@languages;
90 }
91
92 =head2 getTranslatedLanguages
93
94 Returns a reference to an array of hashes:
95
96  my $languages = getTranslatedLanguages();
97  print "Available translated languages:\n";
98  for my $language(@$trlanguages) {
99     print "$language->{language_code}\n"; # language code in iso 639-2
100     print "$language->{language_name}\n"; # language name in native script
101     print "$language->{language_locale_name}\n"; # language name in current locale
102  }
103
104 =cut
105
106 sub getTranslatedLanguages {
107     my ($interface, $theme, $current_language, $which) = @_;
108     my $htdocs;
109     my $all_languages = getAllLanguages();
110     my @languages;
111     my @enabled_languages;
112  
113     if ($interface && $interface eq 'opac' ) {
114         @enabled_languages = split ",", C4::Context->preference('opaclanguages');
115         $htdocs = C4::Context->config('opachtdocs');
116         if ( $theme and -d "$htdocs/$theme" ) {
117             (@languages) = _get_language_dirs($htdocs,$theme);
118         }
119         else {
120             for my $theme ( _get_themes('opac') ) {
121                 push @languages, _get_language_dirs($htdocs,$theme);
122             }
123         }
124     }
125     elsif ($interface && $interface eq 'intranet' ) {
126         @enabled_languages = split ",", C4::Context->preference('language');
127         $htdocs = C4::Context->config('intrahtdocs');
128         if ( $theme and -d "$htdocs/$theme" ) {
129             @languages = _get_language_dirs($htdocs,$theme);
130         }
131         else {
132             foreach my $theme ( _get_themes('intranet') ) {
133                 push @languages, _get_language_dirs($htdocs,$theme);
134             }
135         }
136     }
137     else {
138         @enabled_languages = split ",", C4::Context->preference('opaclanguages');
139         my $htdocs = C4::Context->config('intrahtdocs');
140         foreach my $theme ( _get_themes('intranet') ) {
141             push @languages, _get_language_dirs($htdocs,$theme);
142         }
143         $htdocs = C4::Context->config('opachtdocs');
144         foreach my $theme ( _get_themes('opac') ) {
145             push @languages, _get_language_dirs($htdocs,$theme);
146         }
147         my %seen;
148         $seen{$_}++ for @languages;
149         @languages = keys %seen;
150     }
151     return _build_languages_arrayref($all_languages,\@languages,$current_language,\@enabled_languages);
152 }
153
154 =head2 getAllLanguages
155
156 Returns a reference to an array of hashes:
157
158  my $alllanguages = getAllLanguages();
159  print "Available translated languages:\n";
160  for my $language(@$alllanguages) {
161     print "$language->{language_code}\n";
162     print "$language->{language_name}\n";
163     print "$language->{language_locale_name}\n";
164  }
165
166 =cut
167
168 sub getAllLanguages {
169     my @languages_loop;
170     my $dbh=C4::Context->dbh;
171     my $current_language = shift || 'en';
172     my $sth = $dbh->prepare('SELECT * FROM language_subtag_registry WHERE type=\'language\'');
173     $sth->execute();
174     while (my $language_subtag_registry = $sth->fetchrow_hashref) {
175
176         # pull out all the script descriptions for each language
177         my $sth2= $dbh->prepare("SELECT * FROM language_descriptions LEFT JOIN language_rfc4646_to_iso639 on language_rfc4646_to_iso639.rfc4646_subtag = language_descriptions.subtag WHERE type='language' AND subtag =? AND language_descriptions.lang = ?");
178         $sth2->execute($language_subtag_registry->{subtag},$current_language);
179
180         my $sth3 = $dbh->prepare("SELECT description FROM language_descriptions WHERE type='language' AND subtag=? AND lang=?");
181
182         # add the correct description info
183         while (my $language_descriptions = $sth2->fetchrow_hashref) {
184             $sth3->execute($language_subtag_registry->{subtag},$language_subtag_registry->{subtag});
185             my $native_description;
186             while (my $description = $sth3->fetchrow_hashref) {
187                 $native_description = $description->{description};
188             }
189
190             # fill in the ISO6329 code
191             $language_subtag_registry->{iso639_2_code} = $language_descriptions->{iso639_2_code};
192             # fill in the native description of the language, as well as the current language's translation of that if it exists
193             if ($native_description) {
194                 $language_subtag_registry->{language_description} = $native_description;
195                 $language_subtag_registry->{language_description}.=" ($language_descriptions->{description})" if $language_descriptions->{description};
196             }
197             else {
198                 $language_subtag_registry->{language_description} = $language_descriptions->{description};
199             }
200         }
201         push @languages_loop, $language_subtag_registry;
202     }
203     return \@languages_loop;
204 }
205
206 =head2 _get_themes
207
208 Internal function, returns an array of all available themes.
209
210   (@themes) = &_get_themes('opac');
211   (@themes) = &_get_themes('intranet');
212
213 =cut
214
215 sub _get_themes {
216     my $interface = shift;
217     my $htdocs;
218     my @themes;
219     if ( $interface eq 'intranet' ) {
220         $htdocs = C4::Context->config('intrahtdocs');
221     }
222     else {
223         $htdocs = C4::Context->config('opachtdocs');
224     }
225     opendir D, "$htdocs";
226     my @dirlist = readdir D;
227     foreach my $directory (@dirlist) {
228         # if there's an en dir, it's a valid theme
229         -d "$htdocs/$directory/en" and push @themes, $directory;
230     }
231     return @themes;
232 }
233
234 =head2 _get_language_dirs
235
236 Internal function, returns an array of directory names, excluding non-language directories
237
238 =cut
239
240 sub _get_language_dirs {
241     my ($htdocs,$theme) = @_;
242     my @lang_strings;
243     opendir D, "$htdocs/$theme";
244     for my $lang_string ( readdir D ) {
245         next if $lang_string =~/^\./;
246         next if $lang_string eq 'all';
247         next if $lang_string =~/png$/;
248         next if $lang_string =~/css$/;
249         next if $lang_string =~/CVS$/;
250         next if $lang_string =~/\.txt$/i;     #Don't read the readme.txt !
251         next if $lang_string =~/img|images|famfam/;
252         push @lang_strings, $lang_string;
253     }
254         return (@lang_strings);
255 }
256
257 =head2 _build_languages_arrayref 
258
259 Internal function for building the ref to array of hashes
260
261 FIXME: this could be rewritten and simplified using map
262
263 =cut
264
265 sub _build_languages_arrayref {
266         my ($all_languages,$translated_languages,$current_language,$enabled_languages) = @_;
267         my @translated_languages = @$translated_languages;
268         my @languages_loop; # the final reference to an array of hashrefs
269         my @enabled_languages = @$enabled_languages;
270         # how many languages are enabled, if one, take note, some contexts won't need to display it
271         my %seen_languages; # the language tags we've seen
272         my %found_languages;
273         my $language_groups;
274         my $track_language_groups;
275         my $current_language_regex = regex_lang_subtags($current_language);
276         # Loop through the translated languages
277         for my $translated_language (@translated_languages) {
278             # separate the language string into its subtag types
279             my $language_subtags_hashref = regex_lang_subtags($translated_language);
280
281             # is this language string 'enabled'?
282             for my $enabled_language (@enabled_languages) {
283                 #warn "Checking out if $translated_language eq $enabled_language";
284                 $language_subtags_hashref->{'enabled'} = 1 if $translated_language eq $enabled_language;
285             }
286             
287             # group this language, key by langtag
288             $language_subtags_hashref->{'sublanguage_current'} = 1 if $translated_language eq $current_language;
289             $language_subtags_hashref->{'rfc4646_subtag'} = $translated_language;
290             $language_subtags_hashref->{'native_description'} = language_get_description($language_subtags_hashref->{language},$language_subtags_hashref->{language},'language');
291             $language_subtags_hashref->{'script_description'} = language_get_description($language_subtags_hashref->{script},$language_subtags_hashref->{'language'},'script');
292             $language_subtags_hashref->{'region_description'} = language_get_description($language_subtags_hashref->{region},$language_subtags_hashref->{'language'},'region');
293             $language_subtags_hashref->{'variant_description'} = language_get_description($language_subtags_hashref->{variant},$language_subtags_hashref->{'language'},'variant');
294             $track_language_groups->{$language_subtags_hashref->{'language'}}++;
295             push ( @{ $language_groups->{$language_subtags_hashref->{language}} }, $language_subtags_hashref );
296         }
297         # $key is a language subtag like 'en'
298         while( my ($key, $value) = each %$language_groups) {
299
300             # is this language group enabled? are any of the languages within it enabled?
301             my $enabled;
302             for my $enabled_language (@enabled_languages) {
303                 my $regex_enabled_language = regex_lang_subtags($enabled_language);
304                 $enabled = 1 if $key eq $regex_enabled_language->{language};
305             }
306             push @languages_loop,  {
307                             # this is only use if there is one
308                             rfc4646_subtag => @$value[0]->{rfc4646_subtag},
309                             native_description => language_get_description($key,$key,'language'),
310                             language => $key,
311                             sublanguages_loop => $value,
312                             plural => $track_language_groups->{$key} >1 ? 1 : 0,
313                             current => $current_language_regex->{language} eq $key ? 1 : 0,
314                             group_enabled => $enabled,
315                            };
316         }
317         return \@languages_loop;
318 }
319
320 sub language_get_description {
321     my ($script,$lang,$type) = @_;
322     my $dbh = C4::Context->dbh;
323     my $desc;
324     my $sth = $dbh->prepare("SELECT description FROM language_descriptions WHERE subtag=? AND lang=? AND type=?");
325     #warn "QUERY: SELECT description FROM language_descriptions WHERE subtag=$script AND lang=$lang AND type=$type";
326     $sth->execute($script,$lang,$type);
327     while (my $descriptions = $sth->fetchrow_hashref) {
328         $desc = $descriptions->{'description'};
329     }
330     unless ($desc) {
331         $sth = $dbh->prepare("SELECT description FROM language_descriptions WHERE subtag=? AND lang=? AND type=?");
332         $sth->execute($script,'en',$type);
333         while (my $descriptions = $sth->fetchrow_hashref) {
334             $desc = $descriptions->{'description'};
335         }
336     }
337     return $desc;
338 }
339 =head2 regex_lang_subtags
340
341 This internal sub takes a string composed according to RFC 4646 as
342 an input and returns a reference to a hash containing keys and values
343 for ( language, script, region, variant, extension, privateuse )
344
345 =cut
346
347 sub regex_lang_subtags {
348     my $string = shift;
349
350     # Regex for recognizing RFC 4646 well-formed tags
351     # http://www.rfc-editor.org/rfc/rfc4646.txt
352
353     # regexes based on : http://unicode.org/cldr/data/tools/java/org/unicode/cldr/util/data/langtagRegex.txt
354     # The structure requires no forward references, so it reverses the order.
355     # The uppercase comments are fragments copied from RFC 4646
356     #
357     # Note: the tool requires that any real "=" or "#" or ";" in the regex be escaped.
358
359     my $alpha   = qr/[a-zA-Z]/ ;    # ALPHA
360     my $digit   = qr/[0-9]/ ;   # DIGIT
361     my $alphanum    = qr/[a-zA-Z0-9]/ ; # ALPHA / DIGIT
362     my $x   = qr/[xX]/ ;    # private use singleton
363     my $singleton = qr/[a-w y-z A-W Y-Z]/ ; # other singleton
364     my $s   = qr/[-]/ ; # separator -- lenient parsers will use [-_]
365
366     # Now do the components. The structure is slightly different to allow for capturing the right components.
367     # The notation (?:....) is a non-capturing version of (...): so the "?:" can be deleted if someone doesn't care about capturing.
368
369     my $extlang = qr{(?: $s $alpha{3} )}x ; # *3("-" 3ALPHA)
370     my $language    = qr{(?: $alpha{2,3} | $alpha{4,8} )}x ;
371     #my $language   = qr{(?: $alpha{2,3}$extlang{0,3} | $alpha{4,8} )}x ;   # (2*3ALPHA [ extlang ]) / 4ALPHA / 5*8ALPHA
372
373     my $script  = qr{(?: $alpha{4} )}x ;    # 4ALPHA 
374
375     my $region  = qr{(?: $alpha{2} | $digit{3} )}x ;     # 2ALPHA / 3DIGIT
376
377     my $variantSub  = qr{(?: $digit$alphanum{3} | $alphanum{5,8} )}x ;  # *("-" variant), 5*8alphanum / (DIGIT 3alphanum)
378     my $variant = qr{(?: $variantSub (?: $s$variantSub )* )}x ; # *("-" variant), 5*8alphanum / (DIGIT 3alphanum)
379
380     my $extensionSub    = qr{(?: $singleton (?: $s$alphanum{2,8} )+ )}x ;   # singleton 1*("-" (2*8alphanum))
381     my $extension   = qr{(?: $extensionSub (?: $s$extensionSub )* )}x ; # singleton 1*("-" (2*8alphanum))
382
383     my $privateuse  = qr{(?: $x (?: $s$alphanum{1,8} )+ )}x ;   # ("x"/"X") 1*("-" (1*8alphanum))
384
385     # Define certain grandfathered codes, since otherwise the regex is pretty useless.
386     # Since these are limited, this is safe even later changes to the registry --
387     # the only oddity is that it might change the type of the tag, and thus
388     # the results from the capturing groups.
389     # http://www.iana.org/assignments/language-subtag-registry
390     # Note that these have to be compared case insensitively, requiring (?i) below.
391
392     my $grandfathered   = qr{(?: (?i)
393         en $s GB $s oed
394     |   i $s (?: ami | bnn | default | enochian | hak | klingon | lux | mingo | navajo | pwn | tao | tay | tsu )
395     |   sgn $s (?: BE $s fr | BE $s nl | CH $s de)
396 )}x;
397
398     # For well-formedness, we don't need the ones that would otherwise pass, so they are commented out here
399
400     #   |   art $s lojban
401     #   |   cel $s gaulish
402     #   |   en $s (?: boont | GB $s oed | scouse )
403     #   |   no $s (?: bok | nyn)
404     #   |   zh $s (?: cmn | cmn $s Hans | cmn $s Hant | gan | guoyu | hakka | min | min $s nan | wuu | xiang | yue)
405
406     # Here is the final breakdown, with capturing groups for each of these components
407     # The language, variants, extensions, grandfathered, and private-use may have interior '-'
408
409     #my $root = qr{(?: ($language) (?: $s ($script) )? 40% (?: $s ($region) )? 40% (?: $s ($variant) )? 10% (?: $s ($extension) )? 5% (?: $s ($privateuse) )? 5% ) 90% | ($grandfathered) 5% | ($privateuse) 5% };
410
411     $string =~  qr{^ (?:($language)) (?:$s($script))? (?:$s($region))?  (?:$s($variant))?  (?:$s($extension))?  (?:$s($privateuse))? $}xi;  # |($grandfathered) | ($privateuse) $}xi;
412     my %subtag = (
413         'rfc4646_subtag' => $string,
414         'language' => $1,
415         'script' => $2,
416         'region' => $3,
417         'variant' => $4,
418         'extension' => $5,
419         'privateuse' => $6,
420     );
421     return \%subtag;
422 }
423
424 # Script Direction Resources:
425 # http://www.w3.org/International/questions/qa-scripts
426 sub get_bidi {
427     my ($language_script)= @_;
428     my $dbh = C4::Context->dbh;
429     my $bidi;
430     my $sth = $dbh->prepare('SELECT bidi FROM language_script_bidi WHERE rfc4646_subtag=?');
431     $sth->execute($language_script);
432     while (my $result = $sth->fetchrow_hashref) {
433         $bidi = $result->{'bidi'};
434     }
435     return $bidi;
436 };
437
438 sub accept_language {
439     # referenced http://search.cpan.org/src/CGILMORE/I18N-AcceptLanguage-1.04/lib/I18N/AcceptLanguage.pm
440     # FIXME: since this is only used in Output.pm as of Jan 8 2008, maybe it should be IN Output.pm
441     my ($clientPreferences,$supportedLanguages) = @_;
442     my @languages = ();
443     if ($clientPreferences) {
444         # There should be no whitespace anways, but a cleanliness/sanity check
445         $clientPreferences =~ s/\s//g;
446
447         # Prepare the list of client-acceptable languages
448         foreach my $tag (split(/,/, $clientPreferences)) {
449             my ($language, $quality) = split(/\;/, $tag);
450             $quality =~ s/^q=//i if $quality;
451             $quality = 1 unless $quality;
452             next if $quality <= 0;
453             # We want to force the wildcard to be last
454             $quality = 0 if ($language eq '*');
455             # Pushing lowercase language here saves processing later
456             push(@languages, { quality => $quality,
457                language => $language,
458                lclanguage => lc($language) });
459         }
460     } else {
461         carp "accept_language(x,y) called with no clientPreferences (x).";
462     }
463     # Prepare the list of server-supported languages
464     my %supportedLanguages = ();
465     my %secondaryLanguages = ();
466     foreach my $language (@$supportedLanguages) {
467         # warn "Language supported: " . $language->{language_code};
468         $supportedLanguages{lc($language->{language_code})} = $language->{language_code};
469         if ($language->{language_code} =~ /^([^-]+)-?/) {
470             $secondaryLanguages{lc($1)} = $language->{language_code};
471         }
472     }
473
474     # Reverse sort the list, making best quality at the front of the array
475     @languages = sort { $b->{quality} <=> $a->{quality} } @languages;
476     my $secondaryMatch = '';
477     foreach my $tag (@languages) {
478         if (exists($supportedLanguages{$tag->{lclanguage}})) {
479             # Client en-us eq server en-us
480             return $supportedLanguages{$tag->{language}} if exists($supportedLanguages{$tag->{language}});
481             return $supportedLanguages{$tag->{lclanguage}};
482         } elsif (exists($secondaryLanguages{$tag->{lclanguage}})) {
483             # Client en eq server en-us
484             return $secondaryLanguages{$tag->{language}} if exists($secondaryLanguages{$tag->{language}});
485             return $supportedLanguages{$tag->{lclanguage}};
486         } elsif ($tag->{lclanguage} =~ /^([^-]+)-/ && exists($secondaryLanguages{$1}) && $secondaryMatch eq '') {
487             # Client en-gb eq server en-us
488             $secondaryMatch = $secondaryLanguages{$1};
489         } elsif ($tag->{lclanguage} =~ /^([^-]+)-/ && exists($secondaryLanguages{$1}) && $secondaryMatch eq '') {
490             # FIXME: We just checked the exact same conditional!
491             # Client en-us eq server en
492             $secondaryMatch = $supportedLanguages{$1};
493         } elsif ($tag->{lclanguage} eq '*') {
494         # * matches every language not already specified.
495         # It doesn't care which we pick, so let's pick the default,
496         # if available, then the first in the array.
497         #return $acceptor->defaultLanguage() if $acceptor->defaultLanguage();
498         return $supportedLanguages->[0];
499         }
500     }
501     # No primary matches. Secondary? (ie, en-us requested and en supported)
502     return $secondaryMatch if $secondaryMatch;
503     return undef;   # else, we got nothing.
504 }
505 1;
506
507 __END__
508
509 =head1 AUTHOR
510
511 Joshua Ferraro
512
513 =cut