1use warnings;
2use strict;
3
4package Jifty::Script::Po;
5use base qw(Jifty::Script Class::Accessor::Fast);
6
7use Pod::Usage;
8use File::Copy ();
9use File::Path 'mkpath';
10use Jifty::Config ();
11use Jifty::YAML ();
12use Locale::Maketext::Extract ();
13use File::Find::Rule ();
14use MIME::Types ();
15our $MIME = MIME::Types->new();
16our $LMExtract = Locale::Maketext::Extract->new(
17        # Specify which parser plugins to use
18        plugins => {
19            # Use Perl parser, process files with extension .pl .pm .cgi
20            'Locale::Maketext::Extract::Plugin::PPI' => ['pm','pl'],
21            'tt2' => [ ],
22            'perl' => ['pl','pm','js','json'],
23            'mason' => [ ] ,
24        },
25        verbose => 1,
26);
27
28use constant USE_GETTEXT_STYLE => 1;
29
30__PACKAGE__->mk_accessors(qw/language/);
31
32### Help is below in __DATA__ section
33
34=head2 options
35
36Returns a hash of all the options this script takes. (See the usage message for details)
37
38=cut
39
40sub options {
41    my $self = shift;
42    return (
43        $self->SUPER::options,
44        'l|language=s' => 'language',
45        'dir=s@'       => 'directories',
46        'podir=s'      => 'podir',
47        'js'           => 'js',
48        'template_name=s' => 'template_name',
49    )
50}
51
52
53=head2 run
54
55Runs the "update_catalogs" method.
56
57=cut
58
59
60sub run {
61    my $self = shift;
62    return if $self->print_help;
63
64    Jifty->new(no_handle => 1);
65
66    return $self->_js_gen if $self->{js};
67
68    $self->update_catalogs;
69}
70
71sub _js_gen {
72    my $self = shift;
73    my $static_handler = Jifty::View::Static::Handler->new;
74    my $logger =Log::Log4perl->get_logger("main");
75    for my $file ( @{ Jifty::Web->javascript_libs } ) {
76        next if $file =~ m/^ext/;
77        next if $file =~ m/^yui/;
78        next if $file =~ m/^rico/;
79        my $path = $static_handler->file_path( File::Spec->catdir( 'js', $file ) ) or next;
80
81        $logger->info("Extracting messages from '$path'");
82
83        $LMExtract->extract_file( $path );
84    }
85
86    $LMExtract->set_compiled_entries;
87    $LMExtract->compile(USE_GETTEXT_STYLE);
88
89    Jifty::I18N->new;
90    mkpath ['share/web/static/js/dict'];
91    for my $lang (Jifty::I18N->available_languages) {
92        my $file = "share/web/static/js/dict/$lang.json";
93        $logger->info("Generating $file");
94        open my $fh, '>', $file or die "$file: $!";
95
96        no strict 'refs';
97        print $fh
98            Jifty::JSON::encode_json( { map { my $text = ${"Jifty::I18N::".$lang."::Lexicon"}{$_};
99                                              defined $text ? ( $_ => $text ) : () }
100                                        keys %{$LMExtract->lexicon} } );
101    }
102}
103
104=head2 _check_mime_type FILENAME
105
106This routine returns a mimetype for the file C<FILENAME>.
107
108=cut
109
110sub _check_mime_type {
111    my $self       = shift;
112    my $local_path = shift;
113    my $mimeobj = $MIME->mimeTypeOf($local_path);
114    my $mime_type = ($mimeobj ? $mimeobj->type : "unknown");
115    return if ( $mime_type =~ /^image/ );
116    return 1;
117}
118
119=head2 update_catalogs
120
121Extracts localizable messages from all files in your application, finds
122all your message catalogs and updates them with new and changed messages.
123
124=cut
125
126sub update_catalogs {
127    my $self = shift;
128    my $podir = $self->{'podir'} || Jifty->config->framework('L10N')->{'PoDir'};
129
130    $self->extract_messages;
131    $self->update_catalog( File::Spec->catfile(
132            $podir, $self->pot_name . ".pot"
133        ) );
134
135    if ($self->{'language'}) {
136        $self->update_catalog( File::Spec->catfile(
137            $podir, $self->{'language'} . ".po"
138        ) );
139        return;
140    }
141
142    my @catalogs = grep !m{(^|/)\.svn/}, File::Find::Rule->file->name('*.po')->in(
143        $podir
144    );
145
146    unless ( @catalogs ) {
147        $self->log->error("You have no existing message catalogs.");
148        $self->log->error("Run `jifty po --language <lang>` to create a new one.");
149        $self->log->error("Read `jifty po --help` to get more info.");
150        return
151    }
152
153    foreach my $catalog (@catalogs) {
154        $self->update_catalog( $catalog );
155    }
156}
157
158=head2 update_catalog FILENAME
159
160Reads C<FILENAME>, a message catalog and integrates new or changed
161translations.
162
163=cut
164
165sub update_catalog {
166    my $self       = shift;
167    my $translation = shift;
168    my $logger =Log::Log4perl->get_logger("main");
169    $logger->info( "Updating message catalog '$translation'");
170
171    $LMExtract->read_po($translation) if ( -f $translation && $translation !~ m/pot$/ );
172
173    my $orig_lexicon;
174
175    # Reset previously compiled entries before a new compilation
176    $LMExtract->set_compiled_entries;
177    $LMExtract->compile(USE_GETTEXT_STYLE);
178
179    if ($self->_is_core && !$self->{'template_name'}) {
180        $LMExtract->write_po($translation);
181    }
182    else {
183        $orig_lexicon = $LMExtract->lexicon;
184        my $lexicon = { %$orig_lexicon };
185
186        # XXX: cache core_lm
187        my $core_lm = Locale::Maketext::Extract->new();
188        Locale::Maketext::Lexicon::set_option('allow_empty' => 1);
189        $core_lm->read_po( File::Spec->catfile(
190            Jifty->config->framework('L10N')->{'DefaultPoDir'}, 'jifty.pot'
191        ));
192        Locale::Maketext::Lexicon::set_option('allow_empty' => 0);
193        for (keys %{ $core_lm->lexicon }) {
194            next unless exists $lexicon->{$_};
195            # keep the local entry overriding core if it exists
196            delete $lexicon->{$_} unless length $lexicon->{$_};
197        }
198        $LMExtract->set_lexicon($lexicon);
199
200        $LMExtract->write_po($translation);
201
202        $LMExtract->set_lexicon($orig_lexicon);
203    }
204}
205
206
207=head2 extract_messages
208
209Find all translatable messages in your application, using
210L<Locale::Maketext::Extract>.
211
212=cut
213
214sub extract_messages {
215    my $self = shift;
216    # find all the .pm files in @INC
217    my @files = File::Find::Rule->file->in( @{ $self->{directories} ||
218                                                   [ Jifty->config->framework('Web')->{'TemplateRoot'},
219                                                     'lib', 'bin'] } );
220
221    my $logger =Log::Log4perl->get_logger("main");
222    foreach my $file (@files) {
223        next if $file =~ m{(^|/)[\._]svn/};
224        next if $file =~ m{\~$};
225        next if $file =~ m{\.pod$};
226        next unless $self->_check_mime_type($file );
227        $logger->info("Extracting messages from '$file'");
228        $LMExtract->extract_file($file);
229    }
230
231}
232
233=head2 print_help
234
235Prints out help for the package using pod2usage.
236
237If the user specified --help, prints a brief usage message
238
239If the user specified --man, prints out a manpage
240
241=cut
242
243sub print_help {
244    my $self = shift;
245    return 0 unless $self->{help} || $self->{man};
246
247    # Option handling
248    my $docs = \*DATA;
249    pod2usage( -exitval => 1, -input => $docs ) if $self->{help};
250    pod2usage( -exitval => 0, -verbose => 2, -input => $docs )
251        if $self->{man};
252    return 1;
253}
254
255
256sub _is_core {
257    return 1 if Jifty->config->framework('ApplicationName') eq 'JiftyApp';
258}
259
260=head2 pot_name
261
262Returns the name of the po template.
263
264=cut
265
266sub pot_name {
267    my $self = shift;
268    return $self->{'template_name'} if $self->{'template_name'};
269    return 'jifty' if $self->_is_core;
270    return lc  Jifty->config->framework('ApplicationName');
271}
272
2731;
274
275__DATA__
276
277=head1 NAME
278
279Jifty::Script::Po - Extract translatable strings from your application
280
281=head1 SYNOPSIS
282
283  jifty po --language <lang>  Creates a <lang>.po file for translation
284  jifty po                    Updates all existing po files
285
286 Options:
287   --language         Language to deal with
288   --dir              Additional directories to extract from
289   --js               Generate json files from the current po files
290
291   --help             brief help message
292   --man              full documentation
293
294=head1 OPTIONS
295
296=over 8
297
298=item B<--language>
299
300This script an option, C<--language>, which is optional; it is the
301name of a message catalog to create.
302
303=item B<--dir>
304
305Specify explicit directories to extract from. Can be used multiple
306times.  The default directories will not be extracted if you use this option.
307
308=item B<--template_name>
309
310Specify the name of the po template.  Default to the lower-cased application name.
311
312=item B<--podir>
313
314Specify the directory of the po templates.
315
316=item B<--js>
317
318If C<--js> is given, other options are ignored and the script will
319generate json files for each language under
320F<share/web/static/js/dict> from the current po files.  Before doing
321so, you might want to run C<jifty po> with C<--dir share/web/static/js>
322to include messages from javascript in your po files.
323
324=item B<--help>
325
326Print a brief help message and exits.
327
328=item B<--man>
329
330Prints the manual page and exits.
331
332=back
333
334=head1 DESCRIPTION
335
336Extracts message catalogs for your Jifty app. When run, Jifty will update
337all existing message catalogs, as well as create a new one if you specify
338a --language option.
339
340=cut
341
342