The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Dancer::Plugin::I18N;

use strict;
use warnings;

use Dancer::Plugin;
use Dancer::Config;
use Dancer ':syntax';

use Encode;
use POSIX qw(locale_h);

use I18N::LangTags;
use I18N::LangTags::Detect;
use I18N::LangTags::List;

use Locale::Maketext::Simple;

our $VERSION = '0.40';
our %options = ( Decode => 1,
				 Export => '_loc',
				 #Encoding => 'utf-8',
				 );

# Handler of struct
my $handle = undef;

# Own subs definition
our @array_subs = ();

# Settings
my $settings = undef;

sub _load_i18n_settings {
	$settings = plugin_setting();
	my $n = $settings->{func};
	if (!defined($n))	{
	} elsif (!ref($n) && length($n))	{
		register $n => sub { _localize(@_); };
		push(@array_subs, $n);
	} elsif (ref($n) eq "ARRAY")	{
		foreach my $k (@$n)	{
			register $k => sub { _localize(@_); };
			push(@array_subs, $k);
		}
	} 
	if($settings->{setlocale})	{
		eval { require Locale::Util; 1 };
		if ($@) {
			$settings->{setlocale} = undef;
			error("Couldn't initialize Locale::Util ... ", "$@");
		}
	}
}

# Hook definitions
add_hook(
    before => sub {
		_setup_i18n();

		# Changing and setting language
		my $np = $settings->{name_param} || "lang";
		my $ns = $settings->{name_session} || "language";
		if ($np && $ns)	{
			if (my $p = param $np) {
				my $s = session($ns);
				if ((!$s || $p ne $s) && (installed_languages($p))) {
					session $ns => $p;
					languages($p);
				}
			} elsif (!session($ns))  {
				session $ns => language_tag();
			} elsif (!language_tag(session($ns)))    {
				languages(session($ns));
			}
		}
	},
);

add_hook(
    before_template => sub {
        my $tokens = shift;
        $tokens->{l}         = sub { l(@_) };
        $tokens->{localize}  = sub { localize(@_) };
        $tokens->{language}  = sub { language(@_) };
        $tokens->{language_tag}  = sub { language_tag(@_) };
        $tokens->{languages} = sub { languages(@_) };
        $tokens->{installed_languages} = sub { installed_languages(@_) };

		foreach my $k (@{Dancer::Plugin::I18N::array_subs})	{
			$tokens->{$k} = sub { localize(@_) };		
		}
    },
);

=encoding utf8

=head1 NAME

Dancer::Plugin::I18N - Intenationalization for Dancer

=head1 SYNOPSIS

   # MyApp/I18N/de.po
   msgid "Hello Dancer"
   msgstr "Hallo Tänzerin"
   
   # MyApp/I18N/i_default.po
   msgid "messages.hello.dancer"
   msgstr "Hello Dancer - fallback translation"
   
   # MyApp/I18N/fr.pm
   package myapp::I18N::fr;
   use base 'myapp::I18N';
   our %Lexicon = ( hello => 'bonjour' );
   1;

   package myapp;
   use Dancer;
   use Dancer::Plugin::I18N;
   get '/' => sub {
        my $lang = languages ;
        print @$lang . "\n";
        languages( ['de'] );
        print STDERR localize('Hello Dancer');

        template 'index' 
    };

    # index.tt
    hello in <% languages %> => <% l('hello') %>
    # or
    <% languages('fr') %>This is an <% l('hello') %>
    # or
    <% l('Hello Dancer') %>
    <% l('Hello [_1]', 'Dancer') %>
    <% l('lalala[_1]lalala[_2]', ['test', 'foo']) %>
    <% l('messages.hello.dancer') %>
    # or for big texts
    <% IF language_tag('fr') %>
    ...
    <% ELSE %>
    ...
    <% ENDIF %>


=head1 DESCRIPTION

Supports mo/po files and Maketext classes under your application's I18N namespace.

Dancer::Plugin::I18N add L<Locale::Maketext::Simple> to your L<Dancer> application

=cut

sub _setup_i18n { 

	return if (defined($handle) && ref($handle) eq "HASH");

	_load_i18n_settings() if (!$settings);

	my $lang_path = $settings->{directory} || path(setting('appdir'), 'I18N');

    my $user_opts = $settings->{ maketext_options } || {};
    local %options = (%options, Path => $lang_path, %$user_opts );

	#Locale::Maketext::Simple->import( %options );
	my $self = __PACKAGE__;
    eval <<"";
        package $self;
        Locale::Maketext::Simple->import( \%Dancer\::Plugin\::I18N\::options );

    if ($@) {
        error("Couldn't initialize i18n", "$@");
    }
    else {
        debug("Initialized i18n");
    }

=head1 CONFIGURATION

You can override any parameter sent to L<Locale::Maketext::Simple> by specifying
a C<maketext_options> hashref to the C<Plugin::I18N> in you Dancer application
config file section. For example, the following configuration will override 
the C<Decode> parameter which normally defaults to C<1>:

    plugins:
       I18N:
          directory: I18N
          lang_default: en
          maketext_options:
               Decode: 0

All languages fallback to MyApp::I18N which is mapped onto the i-default
language tag or change this via options 'language_default'. 
If you use arbitrary message keys, use i_default.po to translate
into English, otherwise the message key itself is returned.

Standart directory is in C<I18N>. In this directory are stored every lang files (*.pm|po|mo).

You can defined own function for call locale via settings name C<func>.

    plugins:
       I18N:
          func: "N_"

Or defined as array:

    plugins:
       I18N:
          func: ["N_", "_"]

Now you can call this function in template or in libs.

    # index.tt
    hello in <% languages %> => <% N_('hello') %>


Automaticaly change language via param 'lang', can be change in setting 
via 'name_param' and will be stored in session in tag 'language' or 
can be changed via 'name_session'. When you use this settings, this plugin automaticaly
setting language when you call param 'name_param'. Now if you call every page with 
param 'lang=en' now plugin automatically set new locale.

    plugins:
       I18N:
          name_param: lang
          name_session: language


Automaticaly settings locales must installed L<libintl-perl> in version 1.17 or newer.

    plugins:
       I18N:
          setlocale: "LC_TIME"

Or defined as array:

    plugins:
       I18N:
          setlocale: ["LC_TIME","LC_NUMERIC"]

When you set LC_TIME and use time function for print day name or month name, then will be printed in localed name.

=cut

	# We re-read the list of files in $lang_path
	# Originally tried to detect via namespaces, but this lists the currently set LANG envvar, which may not
	# be a supported language. Also misses out .pm files
	# Is acceptable to re-read this directory once on setup
	my $languages_list = {};
	if (opendir my $langdir, $lang_path) {
		foreach my $entry (readdir $langdir) {
			next unless $entry =~ m/\A (\w+)\.(?:pm|po|mo) \z/xms;
			my $langtag = $1;
			next if $langtag eq "i_default";
			my $language_tag = $langtag;
			#my $language_tag = "$class\::I18N"->get_handle( $langtag )->language_tag;
			# Did use the get_handle, but that caused problems because en became "Default (Fallthru) Language"
			# Just do a simple convert instead
			$language_tag =~ s/_/-/g;
			$languages_list->{ $langtag } = I18N::LangTags::List::name( $language_tag );
		}
		closedir $langdir;
	}

	$handle = {};
	$handle->{installed_languages} = $languages_list;
	
	# Setting language
	my $request = request;
	my @languages = ();
    push @languages,
			I18N::LangTags::implicate_supers(
				I18N::LangTags::Detect->http_accept_langs(
					scalar $request->accept_language
				)
			);
	push (@languages, $settings->{lang_default} || 'i-default');
	$handle->{languages} = \@languages;
	_setup_lang();
}

# Problem is where settings is codepage UTF8 and must be encode to ASCII
sub _txt2ascii	{
	return $_[0] ? Encode::encode("ISO-8859-1", $_[0]) : '';
}

# Setting locale
sub _set_locale	{
	my $lang = shift || return;
	my $charset = shift;

	foreach my $l (@_)	{
		my $s = &_txt2ascii($l);
		foreach my $k ("ALL", "COLLATE", "CTYPE", "MESSAGES", "MONETARY", "NUMERIC", "TIME")	{
			if ($s eq ("LC_" . $k))	{
				no strict 'refs';
				my $c = &{"POSIX::LC_" . $k};
				Locale::Util::web_set_locale ($lang, $charset, $c) if (defined($c));
				last;
			}
		}
	}
}

sub _setup_lang	{
	return if (!$handle || !exists($handle->{languages}));
	no strict 'refs';
	my $c = __PACKAGE__;
	&{ $c . '::_loc_lang' }( @{ $handle->{languages} } );
	#loc_lang( @{ $handle->{languages} } );

	_load_i18n_settings() if (!$settings);

	# Set locale from config
	if (my $s = $settings->{setlocale})	{
		&_set_locale(language_tag(), &_txt2ascii(setting('charset')) || undef,
					 (ref($s) eq "ARRAY" ? @$s : $s));
	}
}

=head1 METHODS

=head2 languages

Contains languages.

   languages(['de_DE']);
   my $lang = languages;
   print join '', @$lang;

=head3 1. Putting new language as first in finded

   languages('de_DE'); 

=head3 2. Erase all and putting new languages as in arrayref

   languages(['de_DE',....,'en']); 

=head3 3. Return putted languages

   languages();

=cut
register languages		=> sub { 
    my $lang = shift;

	_setup_i18n();
	return if (!$handle);

    if ($lang) {
		if (ref($lang) eq "ARRAY")	{
			$handle->{languages} = $lang;
		} else	{
			for (my $i = 0; $i < scalar(@{$handle->{languages}}); $i ++)	{
				if ($handle->{languages}->[$i] eq $lang)	{
					splice(@{$handle->{languages}},$i,1);
					last;
				}
			}
			unshift(@{$handle->{languages}}, $lang);
		}

		_setup_lang();
	} else	{
		return $handle->{languages};
	}
	return;
};

=head2 language

return selected locale in your locales list or check if given locale is used(same as language_tag).

=cut

register language		=> sub { 
	my $lang_test = shift;

	return language_tag($lang_test) if (defined($lang_test));

	my $c = __PACKAGE__;
	my $class = ref $c || $c;
    my $lang = $handle ? "$class\::I18N"->get_handle( @{ $handle->{languages} } ) : "";
    $lang =~ s/.*:://;

    return $lang;
};

=head2 language_tag

return language tag for current locale. The most notable difference from this
method in comparison to C<language()> is typically that languages and regions
are joined with a dash and not an underscore.

    language(); # en_us
    language_tag(); # en-us

=head3 1. Returning selected locale 

	print language_tag();

=head3 2. Test if given locale used

	if (language_tag('en'))	{}

=cut

register language_tag	=> sub { 
	my $lang_test = shift;

	my $c = __PACKAGE__;
	my $class = ref $c || $c;
    my $ret = $handle ? "$class\::I18N"->get_handle( @{ $handle->{languages} } )->language_tag : "";

	if (defined($lang_test))	{
		return 1 if ($ret eq $lang_test || $ret =~ /^$lang_test/);
		return 0;
	}

	return $ret;
};

=head2 installed_languages

Returns a hash of { langtag => "descriptive name for language" } based on language files
in your application's I18N directory. The descriptive name is based on I18N::LangTags::List information.
If the descriptive name is not available, will be undef.

=head3 1. Returning hashref installed language files

	my $l = installed_language();

=head3 2. Test if given locale is installed in hashref

	my $t = installed_language('en');
	

=cut

register installed_languages	=> sub { 
	if (defined($handle))	{
		if (defined($_[0]))	{
			return 1 if($handle->{installed_languages} && $handle->{installed_languages}->{$_[0]});
			return 0;
		}
		return $handle->{installed_languages};
	}
};

=head2 localize | l

Localize text.

    print localize( 'Welcome to Dancer, [_1]', 'sri' );

is same as

    print l( 'Welcome to Dancer, [_1]', 'sri' );

or in template
  
    <% l('Welcome to Dancer, [_1]', 'sri' ) %>

=cut

register localize		=> sub { _localize(@_); };
register l				=> sub { _localize(@_); };

sub _localize {
	_setup_i18n();
	return '' if (scalar(@_) == 0);
	return join '', @_ if (!defined($handle));
	
    #return loc( $_[0], @{ $_[1] } ) if ( ref $_[1] eq 'ARRAY' );
    #return loc(@_);
	no strict 'refs';
	my $c = __PACKAGE__;
	return &{ $c . '::_loc' }( $_[0], @{ $_[1] } ) if ( ref $_[1] eq 'ARRAY' );
	return &{ $c . '::_loc' }(@_);
}

=head1 OUTLINE

    $ dancer -a MyAPP
    $ cd MyAPP
    $ mkdir I18N
    $ xgettext.pl --output=I18N/messages.pot --directory=lib/
    $ ls I18N/
    messages.pot

    $ msginit --input=messages.pot --output=sv.po --locale=sv.utf8
    Created I18N/sv.po.

    $ vim I18N/sv.po

    "Content-Type: text/plain; charset=utf-8\n"

    #: lib/MyApp.pm:50
    msgid "Guest"
    msgstr "Gäst"
    
    #. ($name)
    #: lib/MyApp.pm:54
    msgid "Welcome %1!"
    msgstr "Välkommen %1!"

    $ xgettext.pl --output=I18N/messages.pot --directory=view/
    $ msgmerge --update I18N/sv.po I18N/messages.pot
    . done.

    # compile message catalog to binary format
    $ msgfmt --output-file=I18N/sv.mo I18N/sv.po

=head1 SEE ALSO

L<Dancer>

L<Catalyst::Plugin::I18N>

=head1 AUTHOR

Igor Bujna E<lt>igor.bujna@post.czE<gt>

=head1 ACKNOWLEDGEMENTS

Thanks for authors of L<Catalyst::Plugin::I18N> with idea how make it.

franck cuny E<lt>franck@lumberjaph.netE<gt> for L<Dancer::Plugin:i18n>

=head1 LICENSE

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut

register_plugin;

1;