fix rt-session-viewer mucking up upgrades
[freeside.git] / rt / sbin / extract-message-catalog
1 #!/usr/bin/perl -w 
2 # BEGIN BPS TAGGED BLOCK {{{
3
4 # COPYRIGHT:
5
6 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
7 #                                          <jesse@bestpractical.com>
8
9 # (Except where explicitly superseded by other copyright notices)
10
11
12 # LICENSE:
13
14 # This work is made available to you under the terms of Version 2 of
15 # the GNU General Public License. A copy of that license should have
16 # been provided with this software, but in any event can be snarfed
17 # from www.gnu.org.
18
19 # This work is distributed in the hope that it will be useful, but
20 # WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
22 # General Public License for more details.
23
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27 # 02110-1301 or visit their web page on the internet at
28 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
29
30
31 # CONTRIBUTION SUBMISSION POLICY:
32
33 # (The following paragraph is not intended to limit the rights granted
34 # to you to modify and distribute this software under the terms of
35 # the GNU General Public License and is only of importance to you if
36 # you choose to contribute your changes and enhancements to the
37 # community by submitting them to Best Practical Solutions, LLC.)
38
39 # By intentionally submitting any modifications, corrections or
40 # derivatives to this work, or any other work intended for use with
41 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
42 # you are the copyright holder for those contributions and you grant
43 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
44 # royalty-free, perpetual, license to use, copy, create derivative
45 # works based on those contributions, and sublicense and distribute
46 # those contributions and any derivatives thereof.
47
48 # END BPS TAGGED BLOCK }}}
49 # Portions Copyright 2002 Autrijus Tang <autrijus@autrijus.org>
50
51 use strict;
52
53 use File::Find;
54 use File::Copy;
55 use Regexp::Common;
56 use Carp;
57
58 use vars qw($DEBUG $FILECAT);
59
60 $DEBUG = 1;
61
62 # po dir is for extensions
63 @ARGV = (<lib/RT/I18N/*.po>, <lib/RT/I18N/*.pot>, <po/*.po>, <po/*.pot>) unless @ARGV;
64
65 $FILECAT = {};
66
67 # extract all strings and stuff them into $FILECAT
68 # scan html dir for extensions
69 File::Find::find( { wanted => \&extract_strings_from_code, follow => 1 }, qw(bin sbin lib share html etc) );
70
71 # remove msgid with $ in it.  XXX: perhaps give some warnings here
72 $FILECAT = { map { $_ => $FILECAT->{$_} } grep { !m/\$/ } keys %$FILECAT };
73
74 # ensure proper escaping and [_1] => %1 transformation
75 foreach my $str ( sort keys %{$FILECAT} ) {
76     my $entry = $FILECAT->{$str};
77     my $oldstr = $str;
78
79     $str =~ s/\\/\\\\/g;
80     $str =~ s/\"/\\"/g;
81     $str =~ s/((?<!~)(?:~~)*)\[_(\d+)\]/$1%$2/g;
82     $str =~ s/((?<!~)(?:~~)*)\[([A-Za-z#*]\w*),([^\]]+)\]/"$1%$2(".escape($3).")"/eg;
83     $str =~ s/~([\[\]])/$1/g;
84
85     delete $FILECAT->{$oldstr};
86     $FILECAT->{$str} = $entry;
87 }
88
89 # update all language dictionaries
90 foreach my $dict (@ARGV) {
91     $dict = "lib/RT/I18N/$dict.pot" if ( $dict eq 'rt' );
92     $dict = "lib/RT/I18N/$dict.po" unless -f $dict or $dict =~ m!/!;
93
94     my $lang = $dict;
95     $lang =~ s|.*/||;
96     $lang =~ s|\.po$||;
97     $lang =~ s|\.pot$||;
98
99     update($lang, $dict);
100 }
101
102
103 # {{{ pull strings out of the code.
104
105 sub extract_strings_from_code {
106     my $file = $_;
107
108     local $/;
109     return if ( -d $_ );
110     return if ( $File::Find::dir =~ 'lib/blib|lib/t/autogen|var|m4|local' );
111     return if ( /\.(?:pot|po|bak|gif|png|psd|jpe?g|svg|css|js)$/ );
112     return if ( /~|,D|,B$|extract-message-catalog$|tweak-template-locstring$/ );
113     return if ( /^[\.#]/ );
114     return if ( -f "$_.in" );
115
116     print "Looking at $File::Find::name\n";
117     my $filename = $File::Find::name;
118     $filename =~ s'^\./'';
119     $filename =~ s'\.in$'';
120
121     unless (open _, $file) {
122         print "Cannot open $file for reading ($!), skipping.\n";
123         return;
124     }
125
126     my $re_space_wo_nl = qr{(?!\n)\s};
127     my $re_loc_suffix = qr{$re_space_wo_nl* \# $re_space_wo_nl* loc $re_space_wo_nl* $}x;
128     my $re_loc_qw_suffix = qr{$re_space_wo_nl* \# $re_space_wo_nl* loc_qw $re_space_wo_nl* $}x;
129     my $re_loc_pair_suffix = qr{$re_space_wo_nl* \# $re_space_wo_nl* loc_pair $re_space_wo_nl* $}x;
130     my $re_loc_left_pair_suffix = qr{$re_space_wo_nl* \# $re_space_wo_nl* loc_left_pair $re_space_wo_nl* $}x;
131     my $re_delim = $RE{delimited}{-delim=>q{'"}}{-keep};
132
133     $_ = <_>;
134
135     # Mason filter: <&|/l>...</&>
136     my $line = 1;
137     while (m!\G.*?<&\|/l(.*?)&>(.*?)</&>!sg) {
138         my ( $vars, $str ) = ( $1, $2 );
139         $vars =~ s/[\n\r]//g;
140         $line += ( () = ( $& =~ /\n/g ) );    # cryptocontext!
141         $str =~ s/\\'/\'/g;
142         #print "STR IS $str\n";
143         push @{ $FILECAT->{$str} }, [ $filename, $line, $vars ];
144     }
145
146     # Localization function: loc(...)
147     $line = 1;
148     pos($_) = 0;
149     while (m/\G.*?\bloc$RE{balanced}{-parens=>'()'}{-keep}/sg) {
150         my $match = $1;
151         $line += ( () = ( $& =~ /\n/g ) );    # cryptocontext!
152
153         my ( $vars, $str );
154         if ( $match =~
155                 /\(\s*($re_delim)(.*?)\s*\)$/so ) {
156
157             $str = substr( $1, 1, -1 );       # $str comes before $vars now
158             $vars = $9;
159         }
160         else {
161             next;
162         }
163
164         $vars =~ s/[\n\r]//g;
165         $str  =~ s/\\'/\'/g;
166
167         push @{ $FILECAT->{$str} }, [ $filename, $line, $vars ];
168     }
169
170     # Comment-based mark: "..." # loc
171     $line = 1;
172     pos($_) = 0;
173     while (m/\G.*?($re_delim)[\}\)\],;]*$re_loc_suffix/smgo) {
174         my $str = $1;
175         $line += ( () = ( $& =~ /\n/g ) );    # cryptocontext!
176         unless ( defined $str ) {
177             warn "Couldn't process loc at $filename:$line";
178             next;
179         }
180         $str = substr($str, 1, -1);
181         $str =~ s/\\'/\'/g;
182         push @{ $FILECAT->{$str} }, [ $filename, $line, '' ];
183     }
184
185     # Comment-based qw mark: "qw(...)" # loc_qw
186     $line = 1;
187     pos($_) = 0;
188     while (m/\G.*?(?:(qw\([^)]+\))[\}\)\],;]*)?$re_loc_qw_suffix/smgo) {
189         my $str = $1;
190         $line += ( () = ( $& =~ /\n/g ) );    # cryptocontext!
191         unless ( defined $str ) {
192             warn "Couldn't process loc_qw at $filename:$line";
193             next;
194         }
195         foreach my $value (eval($str)) {
196             push @{ $FILECAT->{$value} }, [ $filename, $line, '' ];
197         }
198     }
199
200     # Comment-based left pair mark: "..." => ... # loc_left_pair
201     $line = 1;
202     pos($_) = 0;
203     while (m/\G.*?(?:(\w+)\s*=>[^#\n]+?)?$re_loc_left_pair_suffix/smgo) {
204         my $key = $1;
205         $line += ( () = ( $& =~ /\n/g ) );    # cryptocontext!
206         unless ( defined $key ) {
207             warn "Couldn't process loc_left_pair at $filename:$line";
208             next;
209         }
210         $key  =~ s/\\'/\'/g;
211         push @{ $FILECAT->{$key} }, [ $filename, $line, '' ];
212     }
213
214     # Comment-based pair mark: "..." => "..." # loc_pair
215     $line = 1;
216     pos($_) = 0;
217     while (m/\G.*?(?:(\w+)\s*=>\s*($re_delim)[\}\)\],;]*)?$re_loc_pair_suffix/smgo) {
218         my $key = $1;
219         my $val = $2;
220         $line += ( () = ( $& =~ /\n/g ) );    # cryptocontext!
221         unless ( defined $key && defined $val ) {
222             warn "Couldn't process loc_pair at $filename:$line";
223             next;
224         }
225         $val = substr($val, 1, -1);
226         $key  =~ s/\\'/\'/g;
227         $val  =~ s/\\'/\'/g;
228         push @{ $FILECAT->{$key} }, [ $filename, $line, '' ];
229         push @{ $FILECAT->{$val} }, [ $filename, $line, '' ];
230     }
231
232     close (_);
233 }
234 # }}} extract from strings
235
236 sub update {
237     my $lang = shift;
238     my $file = shift;
239     my ( %Lexicon, %Header);
240     my $out = '';
241
242     unless (!-e $file or -w $file) {
243         warn "Can't write to $lang, skipping...\n";
244         return;
245     }
246
247     print "Updating $lang...\n";
248
249     my @lines;
250     @lines = (<LEXICON>) if open (LEXICON, $file);
251     @lines = grep { !/^(#(:|\.)\s*|$)/ } @lines;
252     while (@lines) {
253         my $msghdr = "";
254         $msghdr .= shift @lines while ( $lines[0] && $lines[0] !~ /^(#~ )?msgid/ );
255         
256         my $msgid  = "";
257
258 # '#~ ' is the prefix of launchpad for msg that's not found the the source
259 # we'll remove the prefix later so we can still show them with our own mark
260
261         $msgid .= shift @lines while ( $lines[0] && $lines[0] =~ /^(#~ )?(msgid|")/ );
262         my $msgstr = "";
263         $msgstr .= shift @lines while ( $lines[0] && $lines[0] =~ /^(#~ )?(msgstr|")/ );
264
265         last unless $msgid;
266
267         chomp $msgid;
268         chomp $msgstr;
269
270         $msgid  =~ s/^#~ //mg;
271         $msgstr =~ s/^#~ //mg;
272
273         $msgid  =~ s/^msgid "(.*)"\s*?$/$1/m    or warn "$msgid in $file";
274
275         if ( $msgid eq '' ) {
276             # null msgid, msgstr will have head info
277             $msgstr =~ s/^msgstr "(.*)"\s*?$/$1/ms or warn "$msgstr  in $file";
278         }
279         else {
280             $msgstr =~ s/^msgstr "(.*)"\s*?$/$1/m or warn "$msgstr  in $file";
281         }
282
283         if ( $msgid ne ''  ) {
284             for my $msg ( \$msgid, \$msgstr ) {
285                 if ( $$msg =~ /\n/ ) {
286                     my @lines = split /\n/, $$msg;
287                     $$msg =
288                       shift @lines;   # first line don't need to handle any more
289                     for (@lines) {
290                         if (/^"(.*)"\s*$/) {
291                             $$msg .= $1;
292                         }
293                     }
294                 }
295
296                 # convert \\n back to \n
297                 $$msg =~ s/(?!\\)\\n/\n/g;
298             }
299         }
300
301         $Lexicon{$msgid} = $msgstr;
302         $Header{$msgid}  = $msghdr;
303     }
304
305     my $is_english = ( $lang =~ /^en(?:[^A-Za-z]|$)/ );
306
307     foreach my $str ( sort keys %{$FILECAT} ) {
308         $Lexicon{$str} ||= '';
309     }
310     foreach ( sort keys %Lexicon ) {
311         my $f = join ( ' ', sort map $_->[0].":".$_->[1], @{ $FILECAT->{$_} } );
312         my $nospace = $_;
313         $nospace =~ s/ +$//;
314
315         if ( !$Lexicon{$_} and $Lexicon{$nospace} ) {
316             $Lexicon{$_} =
317               $Lexicon{$nospace} . ( ' ' x ( length($_) - length($nospace) ) );
318         }
319
320         next if !length( $Lexicon{$_} ) and $is_english;
321
322         my %seen;
323         $out .= $Header{$_} if exists $Header{$_};
324
325
326
327         next if (!$f && $_ && !$Lexicon{$_});
328         if ( $f && $f !~ /^\s+$/ ) {
329
330             $out .= "#: $f\n";
331         }
332         elsif ($_) {
333             $out .= "#: NOT FOUND IN SOURCE\n";
334         }
335         foreach my $entry ( grep { $_->[2] } @{ $FILECAT->{$_} } ) {
336             my ( $file, $line, $var ) = @{$entry};
337             $var =~ s/^\s*,\s*//;
338             $var =~ s/\s*$//;
339             $out .= "#. ($var)\n" unless $seen{$var}++;
340         }
341         $out .= 'msgid ' . fmt($_) . "msgstr \"$Lexicon{$_}\"\n\n";
342     }
343
344     open PO, ">$file" or die $!;
345     print PO $out;
346     close PO;
347
348     return 1;
349 }
350
351 sub escape {
352     my $text = shift;
353     $text =~ s/\b_(\d+)/%$1/;
354     return $text;
355 }
356
357 sub fmt {
358     my $str = shift;
359     return "\"$str\"\n" unless $str =~ /\n/;
360
361     my $multi_line = ($str =~ /\n(?!\z)/);
362     $str =~ s/\n/\\n"\n"/g;
363
364     if ($str =~ /\n"$/) {
365         chop $str;
366     }
367     else {
368         $str .= "\"\n";
369     }
370     return $multi_line ? qq(""\n"$str) : qq("$str);
371 }
372
373
374 __END__
375 # Local variables:
376 # c-indentation-style: bsd
377 # c-basic-offset: 4
378 # indent-tabs-mode: nil
379 # End:
380 # vim: expandtab shiftwidth=4: