App-I18N

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN


    Translation Status:
        en_US: [                                                  ]  0% (0/8) 
        zh_TW: [======                                            ] 12% (1/8) 


## Auto Translation

Auto translate via Google Translate REST API:

Default backend is google translate REST API, This will translate zh\_TW.po file and translate msgid (en\_US)
to msgstr (zh\_TW):

    $ po auto zh_TW --from en_US

    $ po auto zh_CN --from en_US --to zh_CN

    $ po auto zh_CN --from en_US --overwrite --prompt

    $ po auto --backend google-rest --from en\_US --to zh\_TW

README.md  view on Meta::CPAN

/api/options

/api/entry/list[/{lang}]

/api/entry/unsetlist[/{lang}]

/api/entry/get/{id}

/api/entry/set/{id}/{msgstr}

/api/entry/insert/{lang}/{msgid}/{msgstr}

### For PHP Developers

If you are using gettext extension for your application,
You should use --locale option to generate locale structure.

And `maketext/l10n.php` file for l10n helper class and functions.


## **TODO**

TODO.md  view on Meta::CPAN


# TODO

* Add Apply command to apply messages in file with po string.
    use this command to generate static files.

* global po database by DBIx::Class (sqlite/mysql/pg backend).
* merge/update via global po database.
* provide a web service for managing po files.
* hook google translate plugin to web editor textarea.
* parse msgid from database

    $ po parsedb -hlocalhost -uroot -p1234 dbname table_name column_name

* default charset option.

x mo file 
x locale structure

lib/App/I18N.pm  view on Meta::CPAN


    Translation Status:
        en_US: [                                                  ]  0% (0/8) 
        zh_TW: [======                                            ] 12% (1/8) 


=head2 Auto Translation

Auto translate via Google Translate REST API:

Default backend is google translate REST API, This will translate zh\_TW.po file and translate msgid (en\_US)
to msgstr (zh\_TW):

    $ po auto zh_TW --from en_US

    $ po auto zh_CN --from en_US --to zh_CN

    $ po auto zh_CN --from en_US --overwrite --prompt

    $ po auto --backend google-rest --from en\_US --to zh\_TW

lib/App/I18N/Command/Auto.pm  view on Meta::CPAN




sub options {
    ( 
        'f|from=s' => 'from',
        't|to=s'   => 'to',
        'backend=s' => 'backend',
        'locale'  => 'locale',
        'verbose' => 'verbose',
        'msgstr' => 'msgstr',   # translate from existing msgstr instead of translating from msgid.
        'overwrite' => 'overwrite',  # overwrite existing msgstr
        'p|prompt'    => 'prompt',
    )
}



sub run {
    my ( $self , $lang ) = @_;
    my $logger = $self->logger();

lib/App/I18N/Command/Auto.pm  view on Meta::CPAN


    $logger->info( "Translating: $from_lang_s => $to_lang_s" );

    $logger->info( "Initialing REST::Google" );
    REST::Google::Translate->http_referer('http://google.com');

    binmode STDERR, ":utf8";
    

NEXT_MSGID:
    for my $i ($ext->msgids()) {
        my $msgid  = $i;
        my $msgstr = $ext->msgstr( $i );

        # skip if no msgstr and no overwrite.
        next if $msgstr && ! $self->{overwrite} && ! $self->{msgstr};

        # translate from msgstr
        $i = $msgstr if $msgstr && $self->{msgstr};

        $logger->info( "********" );
        $logger->info( " msgid: $msgid");
        $logger->info( " msgstr: $msgstr" ) if $msgstr;

        $logger->info( " tranlating from msgstr" ) if $self->{msgstr};
        $logger->info( " tranlating from msgid"  ) if ! $self->{msgstr};

        my $retry = 1;
        while($retry--) {
            my $res;
            eval {
                $res = REST::Google::Translate->new(
                    q => $i,
                    langpair => $from_lang_s . '|' . $to_lang_s );
            };

lib/App/I18N/Command/Gen.pm  view on Meta::CPAN


Generate inline i18n dictionary dictionary:

For Perl:

    # This is auto-generated by App::I18N
    # ------------------------------

    package {{app}}::dict::en;
    our $DICT = {
        "msgid" => "msgstr",
        ...
    };

    package {{app}}::dict::zh_tw;
    our $DICT = {
        "msgid" => "msgstr",
        ....
    };
    ...

For PHP:

    en.php:
    <?php
    $DICT = array( 
        "msgid" => "msgstr"
        ....
    );
    ?>
    ....

For JSON

    en.json:
    {
        "msgid..." : "msgstr"
        ...
    };

    zh_tw.json:
    {
        ....
    };

Static JS

lib/App/I18N/DB.pm  view on Meta::CPAN

    my $self = shift;
    $self->dbh->disconnect();
}

sub init_schema {
    my ($self) = shift;
    $self->dbh->do( qq|
        create table po_string (  
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            lang        TEXT,
            msgid       TEXT,
            msgstr      TEXT,
            updated_on  timestamp,
            updated_by  varchar(120));
    |);
}

# by {id}
sub get_entry {
    my ( $self, $id ) = @_;
    my $sth = $self->dbh->prepare(qq{ select * from po_string where id = ? });

lib/App/I18N/DB.pm  view on Meta::CPAN

    return $ret;
}

sub last_id {
    my $self = shift;


}

sub insert {
    my ( $self , $lang , $msgid, $msgstr ) = @_;
    $msgstr = decode_utf8( $msgstr );
    my $sth = $self->dbh->prepare(
        qq| INSERT INTO po_string (  lang , msgid , msgstr ) VALUES ( ? , ? , ? ); |);
    $sth->execute( $lang, $msgid, $msgstr );
}

sub find {
    my ( $self, $lang , $msgid ) = @_;
    my $sth = $self->dbh->prepare(qq| SELECT * FROM po_string WHERE lang = ? AND msgid = ? LIMIT 1;|);
    $sth->execute( $lang, $msgid );
    my $data = $sth->fetchrow_hashref();
    $sth->finish;


    return $data;
}

sub get_unset_entry_list {
    my ($self, $lang ) = @_;
    my $sth;

lib/App/I18N/DB.pm  view on Meta::CPAN

}


sub _entry_sth_to_list {
    my ($self , $sth) = @_;
    my @result;
    while( my $row = $sth->fetchrow_hashref ) {
        push @result, {
            id     => $row->{id},
            lang   => $row->{lang},
            msgid  => $row->{msgid},
            msgstr => $row->{msgstr},
        };
    }
    return \@result;
}


sub get_langlist {
    my $self = shift;
    my $sth = $self->dbh->prepare("select distinct lang from po_string;");

lib/App/I18N/DB.pm  view on Meta::CPAN

    return keys %$hashref;
}

sub write_to_pofile {
    # XXX:

}

sub import_lexicon {
    my ( $self , $lang , $lex ) = @_;
    while ( my ( $msgid, $msgstr ) = each %$lex ) {
        $self->insert( $lang , $msgid , $msgstr );
    }
}


sub import_po {
    my ( $self, $lang, $pofile ) = @_;
    my $lme = App::I18N->lm_extract;
    $lme->read_po($pofile) if -f $pofile && $pofile !~ m/pot$/;
    $self->import_lexicon( $lang , $lme->lexicon );
}

lib/App/I18N/DB.pm  view on Meta::CPAN

    my $lexicon;


    return $lexicon;
}

sub export_po {
    my ( $self , $lang , $pofile ) = @_;
    my $list = $self->get_entry_list( $lang );
    my $lexicon =  {
        map { $_->{msgid} => encode_utf8 $_->{msgstr}; } @$list
    };
    my $lme = App::I18N->lm_extract;
    $lme->set_lexicon( $lexicon );
    $lme->write_po($pofile);
}

=pod
package MsgEntry;
use Any::Moose;
use JSON::XS;
use overload 
    '""' => \&to_string,
    '%{}' => \&to_hash;


has id => ( is => 'rw', isa => 'Int' );
has lang  => ( is => 'rw' , isa => 'Str' );
has msgid => ( is => 'rw' , isa => 'Str' );
has msgstr => ( is => 'rw' , isa => 'Str' );

sub to_hash {
    my $self = shift;
    return (
        id       => $self->id,
        lang     => $self->lang,
        msgid    => $self->msgid,
        msgstr   => $self->msgstr,
    );
}

sub to_string {
    my $self = shift;
    return encode_json( { $self->to_hash } );
}
=cut

lib/App/I18N/Web/Handler.pm  view on Meta::CPAN

    $lme->set_lexicon($o_lexicon);
    $lme->write_po($pofile);
}

sub post {
    my ($self,$path) = @_;
    my $params = $self->request->parameters->mixed;
    use List::MoreUtils qw(zip);

    my $pofile = $params->{pofile};
    my %lexicon = zip @{ $params->{'msgid[]'} } 
        ,@{ $params->{'msgstr[]'} };

    $self->update_po( $pofile , \%lexicon );
    $self->finish({ success => 1 });
}

sub get {
    my ( $self, $path ) = @_;
    $path ||= "/";
    $self->write( Template::Declare->show( $path, $self ) );

lib/App/I18N/Web/Handler.pm  view on Meta::CPAN

/api/podata

/api/entry/list[/{lang}]

/api/entry/unsetlist[/{lang}]

/api/entry/get/{id}

/api/entry/set/{id}/{msgstr}

/api/entry/insert/{lang}/{msgid}/{msgstr}

=cut

sub post {
    my ( $self, $path ) = @_;
    my ( $p1, $p2, @parts ) = split /\//, $path;
    my $pms = $self->request->parameters->mixed;

    if ( $p1 eq 'entry' && $p2 eq 'set' ) {
        my ( $id, $msgstr ) = ( $pms->{id}, $pms->{msgstr} );

lib/App/I18N/Web/Handler.pm  view on Meta::CPAN

        return $self->write($langdata);
    }
    elsif ( $p1 eq 'skip_session' ) {
        $self->application->skip_session( 1 );
        return $self->write({ success => 1 });
    }
    elsif ( $p1 eq 'options' ) {
        return $self->write( $self->application->options );
    }
    elsif ( $p1 eq 'entry' && $p2 eq 'insert' ) {
        my ( $lang, $msgid, $msgstr ) = @parts;

        return $self->write( { error => 'Require language, msgid or msgstr' } ) unless $msgid and $msgstr and $lang;

        $msgstr = decode_utf8($msgstr);

        my $existed = $self->db->find( $lang, $msgid );
        return $self->write( { error => 'MsgID Exists', record => $existed } ) if $existed;

        $self->application->db->insert( $lang, $msgid, $msgstr );
        return $self->write( { success => 1, recordid => $self->application->db->dbh->last_insert_id( undef, undef, 'po_string', 'msgid' ) } );
    }
    elsif ( $p1 eq 'entry' && $p2 eq 'unsetlist' ) {
        my $lang      = shift @parts;
        my $entrylist;
        $entrylist = $self->application->db->get_unset_entry_list($lang);
        return $self->write( { entrylist => $entrylist } );
    }
    elsif ( $p1 eq 'entry' && $p2 eq 'list' ) {
        my $lang      = shift @parts;
        my $entrylist;

lib/App/I18N/Web/View.pm  view on Meta::CPAN

        $logger->info( "$translation doesnt exist." );
    }

    my $LME = App::I18N->lm_extract();
    $LME->read_po( $translation ) if -f $translation;

    my $lex = $LME->lexicon;

    h3 { "Po Web Server: " . $translation };

    # load all po msgid and msgstr
    form { { method is 'post' }

        div {
            outs "Editing po file: " . $translation;
        }

        input { { type is 'hidden',  name is 'pofile' , value is $translation } };

        div { { class is 'msgitem' }
            div { { class is 'msgid column-header' } _("MsgID") }
            div { { class is 'msgstr column-header' } _("MsgStr") }
        };

        # XXX: a better way to read po file ? not to parse every time.
        while( my ($msgid,$msgstr) = each %$lex ) {

            div { { class is 'msgitem' }
                div { { class is 'msgid' }
                    textarea {  { name is 'msgid[]' };
                        outs decode_utf8 $msgid;
                    };
                }

                div { { class is 'msgstr' }
                    textarea {  { name is 'msgstr[]' };
                        outs decode_utf8 $msgstr;
                    };
                }
            }

lib/App/I18N/Web/View.pm  view on Meta::CPAN

template '/entry_edit' => sub {
	div { { id is 'current-message' }
		div { { class is 'navbar' }
			input { { type is 'button' , class is 'prev-message' , value is 'Previous' } };
			input { { type is 'button' , class is 'skip-message' , value is 'Next' } };
			input { { type is 'button' , class is 'next-message' , value is 'Save and Next' } };
		}

		div { { id is 'message-content' }
			div { { id is 'current-lang' } }
			div { { id is 'current-msgid' } }
			textarea { { id is 'current-msgstr' , rows is 6 , cols is 60 , tabindex is 1 } }
		};

		div { { class is 'navbar' }
			input { { type is 'button' , class is 'prev-message' , value is 'Previous' , tabindex is 4 } };
			input { { type is 'button' , class is 'skip-message' , value is 'Next' , tabindex is 3 } };
			input { { type is 'button' , class is 'next-message' , value is 'Save and Next' , tabindex is 2 } };
		}
	}
};

share/static/app.css  view on Meta::CPAN

#langlist li { display: inline; margin: 5px; background: #36D986; color: #007439;  padding: 6px; }
#langlist li:hover { text-decoration: underline; background: #eee; }

#current-lang { color: #999; margin: 10px; }

#current-message { margin: 10px 13%; margin-top: 20px; }
.navbar input { font-size: 16px; padding: 6px; margin-right: 10px; border: 1px solid #ddd; background: #fff; }
.navbar input:hover { background: #eee; }

#current-msgstr,
#current-msgid { 
	margin: 10px; padding: 8px; 
	width: 70%;
	border-top: 1px solid #ddd;
	border-bottom: 1px solid #ddd;
	border-left: 0px;
	border-right: 0px;
	color: #333;
	font-family: Serif;
	font-size: 18px;
}

share/static/app.css  view on Meta::CPAN

textarea,input { 
    font-size: 22px;
    padding: 3px;
    background: #ded;
    color: #666;
}

textarea:focus,
input:focus {  background: #99CCD2;color: #333; font-weight: bold; }

.msgid { width: 300px; float: left; padding:6px; }
.msgstr { width: 400px; float: left; padding:6px; }

.msgid textarea,
.msgid input { width: 300px; }

.msgstr textarea,
.msgstr input { width: 400px; }

.savethis { float: left; padding: 6px; }

.column-header { color: #66A3D2; }
.column-header { color: #66A3D2; }

.msgitem { clear: both; }

share/static/app.js  view on Meta::CPAN





function writeAll(e) {
    var msgids = jQuery(e).parents("form").find( "*[name=msgid[]]" )
        .map( function(i,n) {
            return jQuery(n).val();
         }).get();

    var msgstrs = jQuery(e).parents("form").find( "textarea[name=msgstr[]]" )
        .map( function(i,n) {
            return jQuery(n).val();
         }).get();

    var pofile = jQuery(e).parents("form").find( "input[name=pofile]" ).val();
    //console.log( pofile , msgids , msgstrs );
    jQuery.ajax({
        url: '/',
        type: 'post',
        data: {
            'msgid[]': msgids,
            'msgstr[]': msgstrs,
            'pofile': pofile
         },
        success: function( ) { 
            jQuery.jGrowl( "Updated" );
         }
     });
    return false;
}

share/static/app.js  view on Meta::CPAN



		});
	},
	update: function(entry) { 
		var lang = this.current_lang;
		if(!entry)
			return;

		$('#current-lang').html( "Current Language: " + this.lg.name );
		$('#current-msgid').html( entry.msgid );
		$('#current-msgstr').val( entry.msgstr );
		$('#current-id').val( entry.id );

		var g_el = $('#google-translation');
		var g_el_text = g_el.find('div.text');
		if( g_el.get(0) ) {
			g_el.show();
			g_el_text.html( "Translating..." );
		}
		else {
			g_el = $('<div/>').attr('id','google-translation').append( $('<div/>').addClass('text') );
			$('#current-msgstr').after( g_el );
			g_el = $('#google-translation');
			g_el_text = g_el.find('div.text');
		}

        if( typeof google != "undefined" ) {
            google.language.translate( entry.msgid  , "", lang , function(result) {
                if( result.error ) {
                    g_el_text.html( "Error:" + result.error.message );
                    $.jGrowl( result.error.message , { header: 'Google Translation' , theme: 'error' , sticky: 1 } );
                } else {
                    g_el_text.html( result.translation );
                    var apply = $('<a/>').attr( { href: '#', tabindex: 5 } ).html( 'Apply' ).addClass('apply').click( function(e) {
                        $('#current-msgstr').val( result.translation );
                        g_el.fadeOut('slow');
                    });
                    g_el_text.append( apply );

share/static/jquery.jgrowl.css  view on Meta::CPAN


/** Cross Browser Styling **/
div.center div.jGrowl-notification, div.center div.jGrowl-closer {
	margin-left: 		auto;
	margin-right: 		auto;
}

div.jGrowl div.jGrowl-notification, div.jGrowl div.jGrowl-closer {
	background-color: 		#000;
	opacity: 				.85;
    -ms-filter: 			"progid:DXImageTransform.Microsoft.Alpha(Opacity=85)"; 
    filter: 				progid:DXImageTransform.Microsoft.Alpha(Opacity=85); 
	zoom: 					1;
	width: 					235px;
	padding: 				10px;
	margin-top: 			5px;
	margin-bottom: 			5px;
	font-family: 			Tahoma, Arial, Helvetica, sans-serif;
	font-size: 				1em;
	text-align: 			left;
	display: 				none;
	-moz-border-radius: 	5px;

t/db.t  view on Meta::CPAN

my $db = App::I18N::DB->new();
ok( $db );

$db->insert( 'zh-tw',  'test' , '測試' );

$entry = $db->find( 'zh-tw', 'test' );


ok( $entry );
ok( $entry->{id} );
ok( $entry->{msgid} );
ok( $entry->{lang} );
ok( $entry->{msgstr} );

is( $entry->{msgstr} , '測試' );

my $entries = $db->get_entry_list( 'zh-tw' );
ok( @$entries );
is( scalar(@$entries) , 1 );



( run in 0.922 second using v1.01-cache-2.11-cpan-ceb78f64989 )