2006-12-04 Steve Falkenburg <sfalken@apple.com>
[WebKit-https.git] / WebKitTools / Scripts / extract-localizable-strings
1 #!/usr/bin/perl -w
2
3 # Copyright (C) 2006 Apple Computer, Inc.  All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
7 # are met:
8 #
9 # 1.  Redistributions of source code must retain the above copyright
10 #     notice, this list of conditions and the following disclaimer. 
11 # 2.  Redistributions in binary form must reproduce the above copyright
12 #     notice, this list of conditions and the following disclaimer in the
13 #     documentation and/or other materials provided with the distribution. 
14 # 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15 #     its contributors may be used to endorse or promote products derived
16 #     from this software without specific prior written permission. 
17 #
18 # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 # This script is like the genstrings tool (minus most of the options) with these differences.
30 #
31 #    1) It uses the names UI_STRING and UI_STRING_WITH_KEY for the macros, rather than the macros
32 #       from NSBundle.h, and doesn't support tables (although they would be easy to add).
33 #    2) It supports UTF-8 in key strings (and hence uses "" strings rather than @"" strings;
34 #       @"" strings only reliably support ASCII since they are decoded based on the system encoding
35 #       at runtime, so give different results on US and Japanese systems for example).
36 #    3) It looks for strings that are not marked for localization, using both macro names that are
37 #       known to be used for debugging in Intrigue source code and an exceptions file.
38 #    4) It finds the files to work on rather than taking them as parameters, and also uses a
39 #       hardcoded location for both the output file and the exceptions file.
40 #       It would have been nice to use the project to find the source files, but it's too hard to
41 #       locate source files after parsing a .pbxproj file.
42
43 # The exceptions file has a list of strings in quotes, filenames, and filename/string pairs separated by :.
44
45 use strict;
46
47 my $exceptionsFile = "English.lproj/StringsNotToBeLocalized.txt";
48 my $stringsFile = "English.lproj/Localizable.strings";
49 my %isDebugMacro = ( ASSERT_WITH_MESSAGE => 1, LOG_ERROR => 1, ERROR => 1, NSURL_ERROR => 1, FATAL => 1, LOG => 1, dprintf => 1, NSException => 1, NSLog => 1, printf => 1 );
50
51 die "usage: extract-localizable-strings\n" if @ARGV != 0;
52
53 my $sawError = 0;
54
55 my $localizedCount = 0;
56 my $keyCollisionCount = 0;
57 my $notLocalizedCount = 0;
58 my $NSLocalizeCount = 0;
59
60 my %exception;
61 my %usedException;
62
63 if (open EXCEPTIONS, $exceptionsFile) {
64     while (<EXCEPTIONS>) {
65         chomp;
66         if (/^"([^\\"]|\\.)*"$/ or /^[-_\/\w.]+.(h|m|mm|cpp)$/ or /^[-_\/\w.]+.(h|m|mm|cpp):"([^\\"]|\\.)*"$/) {
67             if ($exception{$_}) {
68                 print "$exceptionsFile:$.:exception for $_ appears twice\n";
69                 print "$exceptionsFile:$exception{$_}:first appearance\n";
70             } else {
71                 $exception{$_} = $.;
72             }
73         } else {
74             print "$exceptionsFile:$.:syntax error\n";
75         }
76     }
77     close EXCEPTIONS;
78 }
79
80 my @files = ( split "\n", `find . -name "*.h" -o -name "*.m" -o -name "*.mm" -o -name "*.cpp"` );
81
82 for my $file (sort @files) {
83
84     next if $file =~ /\/WebLocalizableStrings\.h$/;
85     next if $file =~ /\/icu\//;
86     next if $file =~ /\/COM\//;
87
88     $file =~ s-^./--;
89
90     open SOURCE, $file or die "can't open $file\n";
91     
92     my $inComment = 0;
93     
94     my $expected = "";
95     my $macroLine;
96     my $macro;
97     my $UIString;
98     my $key;
99     my $comment;
100     
101     my $string;
102     my $stringLine;
103     my $nestingLevel;
104     
105     my $previousToken = "";
106
107     while (<SOURCE>) {
108         chomp;
109         
110         # Handle continued multi-line comment.
111         if ($inComment) {
112             next unless s-.*\*/--;
113             $inComment = 0;
114         }
115     
116         # Handle all the tokens in the line.
117         while (s-^\s*([#\w]+|/\*|//|[^#\w/'"()\[\],]+|.)--) {
118             my $token = $1;
119             
120             if ($token eq "\"") {
121                 if ($expected and $expected ne "a quoted string") {
122                     print "$file:$.:ERROR:found a quoted string but expected $expected\n";
123                     $sawError = 1;
124                     $expected = "";
125                 }
126                 if (s-^(([^\\$token]|\\.)*?)$token--) {
127                     if (!defined $string) {
128                         $stringLine = $.;
129                         $string = $1;
130                     } else {
131                         $string .= $1;
132                     }
133                 } else {
134                     print "$file:$.:ERROR:mismatched quotes\n";
135                     $sawError = 1;
136                     $_ = "";
137                 }
138                 next;
139             }
140             
141             if (defined $string) {
142 handleString:
143                 if ($expected) {
144                     if (!defined $UIString) {
145                         # FIXME: Validate UTF-8 here?
146                         $UIString = $string;
147                         $expected = ",";
148                     } elsif ($macro eq "UI_STRING_KEY" and !defined $key) {
149                         # FIXME: Validate UTF-8 here?
150                         $key = $string;
151                         $expected = ",";
152                     } elsif (!defined $comment) {
153                         # FIXME: Validate UTF-8 here?
154                         $comment = $string;
155                         $expected = ")";
156                     }
157                 } else {
158                     if (defined $nestingLevel) {
159                         # In a debug macro, no need to localize.
160                     } elsif ($previousToken eq "#include" or $previousToken eq "#import") {
161                         # File name, no need to localize.
162                     } elsif ($previousToken eq "extern" and $string eq "C") {
163                         # extern "C", no need to localize.
164                     } elsif ($string eq "") {
165                         # Empty string can sometimes be localized, but we need not complain if not.
166                     } elsif ($exception{$file}) {
167                         $usedException{$file} = 1;
168                     } elsif ($exception{"\"$string\""}) {
169                         $usedException{"\"$string\""} = 1;
170                     } elsif ($exception{"$file:\"$string\""}) {
171                         $usedException{"$file:\"$string\""} = 1;
172                     } else {
173                         print "$file:$stringLine:\"$string\" is not marked for localization\n";
174                         $notLocalizedCount++;
175                     }
176                 }
177                 $string = undef;
178                 last if !defined $token;
179             }
180             
181             $previousToken = $token;
182
183             if ($token =~ /^NSLocalized/ && $token !~ /NSLocalizedDescriptionKey/ && $token !~ /NSLocalizedStringFromTableInBundle/) {
184                 print "$file:$.:ERROR:found a use of an NSLocalized macro; not supported\n";
185                 $nestingLevel = 0 if !defined $nestingLevel;
186                 $sawError = 1;
187                 $NSLocalizeCount++;
188             } elsif ($token eq "/*") {
189                 if (!s-^.*?\*/--) {
190                     $_ = ""; # If the comment doesn't end, discard the result of the line and set flag
191                     $inComment = 1;
192                 }
193             } elsif ($token eq "//") {
194                 $_ = ""; # Discard the rest of the line
195             } elsif ($token eq "'") {
196                 if (!s-([^\\]|\\.)'--) { #' <-- that single quote makes the Project Builder editor less confused
197                     print "$file:$.:ERROR:mismatched single quote\n";
198                     $sawError = 1;
199                     $_ = "";
200                 }
201             } else {
202                 if ($expected and $expected ne $token) {
203                     print "$file:$.:ERROR:found $token but expected $expected\n";
204                     $sawError = 1;
205                     $expected = "";
206                 }
207                 if ($token eq "UI_STRING" or $token eq "UI_STRING_KEY") {
208                     $expected = "(";
209                     $macro = $token;
210                     $UIString = undef;
211                     $key = undef;
212                     $comment = undef;
213                     $macroLine = $.;
214                 } elsif ($token eq "(" or $token eq "[") {
215                     ++$nestingLevel if defined $nestingLevel;
216                     $expected = "a quoted string" if $expected;
217                 } elsif ($token eq ",") {
218                     $expected = "a quoted string" if $expected;
219                 } elsif ($token eq ")" or $token eq "]") {
220                     $nestingLevel = undef if defined $nestingLevel && !--$nestingLevel;
221                     if ($expected) {
222                         $key = $UIString if !defined $key;
223                         HandleUIString($UIString, $key, $comment, $file, $macroLine);
224                         $macro = "";
225                         $expected = "";
226                         $localizedCount++;
227                     }
228                 } elsif ($isDebugMacro{$token}) {
229                     $nestingLevel = 0 if !defined $nestingLevel;
230                 }
231             }
232         }
233             
234     }
235     
236     goto handleString if defined $string;
237     
238     if ($expected) {
239         print "$file:ERROR:reached end of file but expected $expected\n";
240         $sawError = 1;
241     }
242     
243     close SOURCE;
244 }
245
246 my %stringByKey;
247 my %commentByKey;
248 my %fileByKey;
249 my %lineByKey;
250
251 sub HandleUIString
252 {
253     my ($string, $key, $comment, $file, $line) = @_;
254
255     my $bad = 0;
256     if (grep { $_ == 0xFFFD } unpack "U*", $string) {
257         print "$file:$line:ERROR:string for translation has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
258         $bad = 1;
259     }
260     if ($string ne $key && grep { $_ == 0xFFFD } unpack "U*", $key) {
261         print "$file:$line:ERROR:key has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
262         $bad = 1;
263     }
264     if (grep { $_ == 0xFFFD } unpack "U*", $comment) {
265         print "$file:$line:ERROR:comment for translation has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
266         $bad = 1;
267     }
268     if ($bad) {
269         $sawError = 1;
270         return;
271     }
272     
273     if ($stringByKey{$key} && $stringByKey{$key} ne $string) {
274         print "$file:$line:encountered the same key, \"$key\", twice, with different strings\n";
275         print "$fileByKey{$key}:$lineByKey{$key}:previous occurrence\n";
276         $keyCollisionCount++;
277         return;
278     }
279     if ($commentByKey{$key} && $commentByKey{$key} ne $comment) {
280         print "$file:$line:encountered the same key, \"$key\", twice, with different comments\n";
281         print "$fileByKey{$key}:$lineByKey{$key}:previous occurrence\n";
282         $keyCollisionCount++;
283         return;
284     }
285
286     $fileByKey{$key} = $file;
287     $lineByKey{$key} = $line;
288     $stringByKey{$key} = $string;
289     $commentByKey{$key} = $comment;
290 }
291
292 print "\n" if $sawError || $notLocalizedCount || $NSLocalizeCount;
293
294 my @unusedExceptions = sort grep { !$usedException{$_} } keys %exception;
295 if (@unusedExceptions) {
296     for my $unused (@unusedExceptions) {
297         print "$exceptionsFile:$exception{$unused}:exception $unused not used\n";
298     }
299     print "\n";
300 }
301
302 print "$localizedCount localizable strings\n" if $localizedCount;
303 print "$keyCollisionCount key collisions\n" if $keyCollisionCount;
304 print "$notLocalizedCount strings not marked for localization\n" if $notLocalizedCount;
305 print "$NSLocalizeCount uses of NSLocalize\n" if $NSLocalizeCount;
306 print scalar(@unusedExceptions), " unused exceptions\n" if @unusedExceptions;
307
308 if ($sawError) {
309     print "\nErrors encountered. Exiting without writing a $stringsFile file.\n";
310     exit 1;
311 }
312
313 my $localizedStrings = "";
314
315 for my $key (sort keys %commentByKey) {
316     $localizedStrings .= "/* $commentByKey{$key} */\n\"$key\" = \"$stringByKey{$key}\";\n\n";
317 }
318
319 # Write out the strings file in UTF-16 with a BOM.
320 open STRINGS, ">", $stringsFile or die;
321 utf8::decode($localizedStrings) if $^V ge chr(5).chr(8);
322 print STRINGS pack "n*", (0xFEFF, unpack "U*", $localizedStrings);
323 close STRINGS;