Implement <attachment> element appearance on Mac
[WebKit-https.git] / Source / WebCore / extract-localizable-strings.pl
1 #!/usr/bin/perl -w
2
3 # Copyright (C) 2006, 2007, 2009, 2010, 2013 Apple 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 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 use File::Compare;
47 use File::Copy;
48 use Getopt::Long;
49 no warnings 'deprecated';
50
51 sub UnescapeHexSequence($);
52
53 my %isDebugMacro = ( ASSERT_WITH_MESSAGE => 1, LOG_ERROR => 1, ERROR => 1, NSURL_ERROR => 1, FATAL => 1, LOG => 1, LOG_WARNING => 1, UI_STRING_LOCALIZE_LATER => 1, UI_STRING_LOCALIZE_LATER_KEY => 1, LPCTSTR_UI_STRING_LOCALIZE_LATER => 1, UNLOCALIZED_STRING => 1, UNLOCALIZED_LPCTSTR => 1, dprintf => 1, NSException => 1, NSLog => 1, printf => 1 );
54
55 my $verify;
56 my $exceptionsFile;
57 my @directoriesToSkip = ();
58 my $treatWarningsAsErrors;
59
60 my %options = (
61     'verify' => \$verify,
62     'exceptions=s' => \$exceptionsFile,
63     'skip=s' => \@directoriesToSkip,
64     'treat-warnings-as-errors' => \$treatWarningsAsErrors,
65 );
66
67 GetOptions(%options);
68
69 @ARGV >= 2 or die "Usage: extract-localizable-strings [--verify] [--treat-warnings-as-errors] [--exceptions <exceptions file>] <file to update> [--skip directory | directory]...\nDid you mean to run update-webkit-localizable-strings instead?\n";
70
71 -f $exceptionsFile or die "Couldn't find exceptions file $exceptionsFile\n" unless !defined $exceptionsFile;
72
73 my $fileToUpdate = shift @ARGV;
74 -f $fileToUpdate or die "Couldn't find file to update $fileToUpdate\n";
75
76 my $warnAboutUnlocalizedStrings = defined $exceptionsFile;
77
78 my @directories = ();
79 if (@ARGV < 1) {
80     push(@directories, ".");
81 } else {
82     for my $dir (@ARGV) {
83         push @directories, $dir;
84     }
85 }
86
87 my $sawError = 0;
88
89 my $localizedCount = 0;
90 my $keyCollisionCount = 0;
91 my $notLocalizedCount = 0;
92 my $NSLocalizeCount = 0;
93
94 my %exception;
95 my %usedException;
96
97 sub emitWarning($$$)
98 {
99     my ($file, $line, $message) = @_;
100     my $prefix = $treatWarningsAsErrors ? "" : "warning: ";
101     print "$file:$line: $prefix$message\n";
102     $sawError = 1 if $treatWarningsAsErrors;
103 }
104
105 if (defined $exceptionsFile && open EXCEPTIONS, $exceptionsFile) {
106     while (<EXCEPTIONS>) {
107         chomp;
108         if (/^"([^\\"]|\\.)*"$/ or /^[-_\/\w\s.]+.(h|m|mm|c|cpp)$/ or /^[-_\/\w\s.]+.(h|m|mm|c|cpp):"([^\\"]|\\.)*"$/) {
109             if ($exception{$_}) {
110                 emitWarning($exceptionsFile, $., "exception for $_ appears twice");
111                 emitWarning($exceptionsFile, $exception{$_}, "first appearance");
112             } else {
113                 $exception{$_} = $.;
114             }
115         } else {
116             emitWarning($exceptionsFile, $., "syntax error");
117         }
118     }
119     close EXCEPTIONS;
120 }
121
122 my $quotedDirectoriesString = '"' . join('" "', @directories) . '"';
123 for my $dir (@directoriesToSkip) {
124     $quotedDirectoriesString .= ' -path "' . $dir . '" -prune -o';
125 }
126
127 my @files = ( split "\n", `find $quotedDirectoriesString \\( -name "*.h" -o -name "*.m" -o -name "*.mm" -o -name "*.c" -o -name "*.cpp" \\)` );
128
129 for my $file (sort @files) {
130     next if $file =~ /\/\w+LocalizableStrings\w*\.h$/ || $file =~ /\/LocalizedStrings\.h$/;
131
132     $file =~ s-^./--;
133
134     open SOURCE, $file or die "can't open $file\n";
135     
136     my $inComment = 0;
137     
138     my $expected = "";
139     my $macroLine;
140     my $macro;
141     my $UIString;
142     my $key;
143     my $comment;
144     
145     my $string;
146     my $stringLine;
147     my $nestingLevel;
148     
149     my $previousToken = "";
150
151     while (<SOURCE>) {
152         chomp;
153         
154         # Handle continued multi-line comment.
155         if ($inComment) {
156             next unless s-.*\*/--;
157             $inComment = 0;
158         }
159
160         next unless defined $nestingLevel or /(\"|\/\*)/;
161     
162         # Handle all the tokens in the line.
163         while (s-^\s*([#\w]+|/\*|//|[^#\w/'"()\[\],]+|.)--) {
164             my $token = $1;
165             
166             if ($token eq "\"") {
167                 if ($expected and $expected ne "a quoted string") {
168                     print "$file:$.: found a quoted string but expected $expected\n";
169                     $sawError = 1;
170                     $expected = "";
171                 }
172                 if (s-^(([^\\$token]|\\.)*?)$token--) {
173                     if (!defined $string) {
174                         $stringLine = $.;
175                         $string = $1;
176                     } else {
177                         $string .= $1;
178                     }
179                 } else {
180                     print "$file:$.: mismatched quotes\n";
181                     $sawError = 1;
182                     $_ = "";
183                 }
184                 next;
185             }
186             
187             if (defined $string) {
188 handleString:
189                 if ($expected) {
190                     if (!defined $UIString) {
191                         # FIXME: Validate UTF-8 here?
192                         $UIString = $string;
193                         $expected = ",";
194                     } elsif (($macro =~ /(WEB_)?UI_STRING_KEY(_INTERNAL)?$/) and !defined $key) {
195                         # FIXME: Validate UTF-8 here?
196                         $key = $string;
197                         $expected = ",";
198                     } elsif (!defined $comment) {
199                         # FIXME: Validate UTF-8 here?
200                         $comment = $string;
201                         $expected = ")";
202                     }
203                 } else {
204                     if (defined $nestingLevel) {
205                         # In a debug macro, no need to localize.
206                     } elsif ($previousToken eq "#include" or $previousToken eq "#import") {
207                         # File name, no need to localize.
208                     } elsif ($previousToken eq "extern" and $string eq "C") {
209                         # extern "C", no need to localize.
210                     } elsif ($string eq "") {
211                         # Empty string can sometimes be localized, but we need not complain if not.
212                     } elsif ($exception{$file}) {
213                         $usedException{$file} = 1;
214                     } elsif ($exception{"\"$string\""}) {
215                         $usedException{"\"$string\""} = 1;
216                     } elsif ($exception{"$file:\"$string\""}) {
217                         $usedException{"$file:\"$string\""} = 1;
218                     } else {
219                         emitWarning($file, $stringLine, "\"$string\" is not marked for localization") if $warnAboutUnlocalizedStrings;
220                         $notLocalizedCount++;
221                     }
222                 }
223                 $string = undef;
224                 last if !defined $token;
225             }
226             
227             $previousToken = $token;
228
229             if ($token =~ /^NSLocalized/ && $token !~ /NSLocalizedDescriptionKey/ && $token !~ /NSLocalizedStringFromTableInBundle/ && $token !~ /NSLocalizedFileSizeDescription/ && $token !~ /NSLocalizedDescriptionKey/ && $token !~ /NSLocalizedRecoverySuggestionErrorKey/) {
230                 print "$file:$.: found a use of an NSLocalized macro ($token); not supported\n";
231                 $nestingLevel = 0 if !defined $nestingLevel;
232                 $sawError = 1;
233                 $NSLocalizeCount++;
234             } elsif ($token eq "/*") {
235                 if (!s-^.*?\*/--) {
236                     $_ = ""; # If the comment doesn't end, discard the result of the line and set flag
237                     $inComment = 1;
238                 }
239             } elsif ($token eq "//") {
240                 $_ = ""; # Discard the rest of the line
241             } elsif ($token eq "'") {
242                 if (!s-([^\\]|\\.)'--) { #' <-- that single quote makes the Project Builder editor less confused
243                     print "$file:$.: mismatched single quote\n";
244                     $sawError = 1;
245                     $_ = "";
246                 }
247             } else {
248                 if ($expected and $expected ne $token) {
249                     print "$file:$.: found $token but expected $expected\n";
250                     $sawError = 1;
251                     $expected = "";
252                 }
253                 if ($token =~ /(WEB_)?UI_STRING(_KEY)?(_INTERNAL)?$/) {
254                     $expected = "(";
255                     $macro = $token;
256                     $UIString = undef;
257                     $key = undef;
258                     $comment = undef;
259                     $macroLine = $.;
260                 } elsif ($token eq "(" or $token eq "[") {
261                     ++$nestingLevel if defined $nestingLevel;
262                     $expected = "a quoted string" if $expected;
263                 } elsif ($token eq ",") {
264                     $expected = "a quoted string" if $expected;
265                 } elsif ($token eq ")" or $token eq "]") {
266                     $nestingLevel = undef if defined $nestingLevel && !--$nestingLevel;
267                     if ($expected) {
268                         $key = $UIString if !defined $key;
269                         HandleUIString($UIString, $key, $comment, $file, $macroLine);
270                         $macro = "";
271                         $expected = "";
272                         $localizedCount++;
273                     }
274                 } elsif ($isDebugMacro{$token}) {
275                     $nestingLevel = 0 if !defined $nestingLevel;
276                 }
277             }
278         }
279             
280     }
281     
282     goto handleString if defined $string;
283     
284     if ($expected) {
285         print "$file: reached end of file but expected $expected\n";
286         $sawError = 1;
287     }
288     
289     close SOURCE;
290 }
291
292 # Unescapes C language hexadecimal escape sequences.
293 sub UnescapeHexSequence($)
294 {
295     my ($originalStr) = @_;
296
297     my $escapedStr = $originalStr;
298     my $unescapedStr = "";
299
300     for (;;) {
301         if ($escapedStr =~ s-^\\x([[:xdigit:]]+)--) {
302             if (256 <= hex($1)) {
303                 print "Hexadecimal escape sequence out of range: \\x$1\n";
304                 return undef;
305             }
306             $unescapedStr .= pack("H*", $1);
307         } elsif ($escapedStr =~ s-^(.)--) {
308             $unescapedStr .= $1;
309         } else {
310             return $unescapedStr;
311         }
312     }
313 }
314
315 my %stringByKey;
316 my %commentByKey;
317 my %fileByKey;
318 my %lineByKey;
319
320 sub HandleUIString
321 {
322     my ($string, $key, $comment, $file, $line) = @_;
323
324     my $bad = 0;
325     $string = UnescapeHexSequence($string);
326     if (!defined($string)) {
327         print "$file:$line: string has an illegal hexadecimal escape sequence\n";
328         $bad = 1;
329     }
330     $key = UnescapeHexSequence($key);
331     if (!defined($key)) {
332         print "$file:$line: key has an illegal hexadecimal escape sequence\n";
333         $bad = 1;
334     }
335     $comment = UnescapeHexSequence($comment);
336     if (!defined($comment)) {
337         print "$file:$line: comment has an illegal hexadecimal escape sequence\n";
338         $bad = 1;
339     }
340     if (grep { $_ == 0xFFFD } unpack "U*", $string) {
341         print "$file:$line: string for translation has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
342         $bad = 1;
343     }
344     if ($string ne $key && grep { $_ == 0xFFFD } unpack "U*", $key) {
345         print "$file:$line: key has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
346         $bad = 1;
347     }
348     if (grep { $_ == 0xFFFD } unpack "U*", $comment) {
349         print "$file:$line: comment for translation has illegal UTF-8 -- most likely a problem with the Text Encoding of the source file\n";
350         $bad = 1;
351     }
352     if ($bad) {
353         $sawError = 1;
354         return;
355     }
356     
357     if ($stringByKey{$key} && $stringByKey{$key} ne $string) {
358         emitWarning($file, $line, "encountered the same key, \"$key\", twice, with different strings");
359         emitWarning($fileByKey{$key}, $lineByKey{$key}, "previous occurrence");
360         $keyCollisionCount++;
361         return;
362     }
363     if ($commentByKey{$key} && $commentByKey{$key} ne $comment) {
364         emitWarning($file, $line, "encountered the same key, \"$key\", twice, with different comments");
365         emitWarning($fileByKey{$key}, $lineByKey{$key}, "previous occurrence");
366         $keyCollisionCount++;
367         return;
368     }
369
370     $fileByKey{$key} = $file;
371     $lineByKey{$key} = $line;
372     $stringByKey{$key} = $string;
373     $commentByKey{$key} = $comment;
374 }
375
376 print "\n" if $sawError || $notLocalizedCount || $NSLocalizeCount;
377
378 my @unusedExceptions = sort grep { !$usedException{$_} } keys %exception;
379 if (@unusedExceptions) {
380     for my $unused (@unusedExceptions) {
381         emitWarning($exceptionsFile, $exception{$unused}, "exception $unused not used");
382     }
383     print "\n";
384 }
385
386 print "$localizedCount localizable strings\n" if $localizedCount;
387 print "$keyCollisionCount key collisions\n" if $keyCollisionCount;
388 print "$notLocalizedCount strings not marked for localization\n" if $notLocalizedCount;
389 print "$NSLocalizeCount uses of NSLocalize\n" if $NSLocalizeCount;
390 print scalar(@unusedExceptions), " unused exceptions\n" if @unusedExceptions;
391
392 if ($sawError) {
393     print "\nErrors encountered. Exiting without writing to $fileToUpdate.\n";
394     exit 1;
395 }
396
397 my $localizedStrings = "";
398
399 for my $key (sort keys %commentByKey) {
400     $localizedStrings .= "/* $commentByKey{$key} */\n\"$key\" = \"$stringByKey{$key}\";\n\n";
401 }
402
403 if (-e "$fileToUpdate") {
404     if (!$verify) {
405         # Write out the strings file as UTF-8
406         my $temporaryFile = "$fileToUpdate.updated";
407         open STRINGS, ">", $temporaryFile or die;
408         print STRINGS $localizedStrings;
409         close STRINGS;
410
411         # Avoid updating the target file's modification time if the contents have not changed.
412         if (compare($temporaryFile, $fileToUpdate)) {
413             move($temporaryFile, $fileToUpdate);
414         } else {
415             unlink $temporaryFile;
416         }
417     } else {
418         open STRINGS, $fileToUpdate or die;
419
420         my $lastComment;
421         my $line;
422
423         while (<STRINGS>) {
424             chomp;
425
426             next if (/^\s*$/);
427
428             if (/^\/\* (.*) \*\/$/) {
429                 $lastComment = $1;
430             } elsif (/^"((?:[^\\]|\\[^"])*)"\s*=\s*"((?:[^\\]|\\[^"])*)";$/) #
431             {
432                 my $string = delete $stringByKey{$1};
433                 if (!defined $string) {
434                     print "$fileToUpdate:$.: unused key \"$1\"\n";
435                     $sawError = 1;
436                 } else {
437                     if (!($string eq $2)) {
438                         print "$fileToUpdate:$.: unexpected value \"$2\" for key \"$1\"\n";
439                         print "$fileByKey{$1}:$lineByKey{$1}: expected value \"$string\" defined here\n";
440                         $sawError = 1;
441                     }
442                     if (!($lastComment eq $commentByKey{$1})) {
443                         print "$fileToUpdate:$.: unexpected comment /* $lastComment */ for key \"$1\"\n";
444                         print "$fileByKey{$1}:$lineByKey{$1}: expected comment /* $commentByKey{$1} */ defined here\n";
445                         $sawError = 1;
446                     }
447                 }
448             } else {
449                 print "$fileToUpdate:$.: line with unexpected format: $_\n";
450                 $sawError = 1;
451             }
452         }
453
454         for my $missing (keys %stringByKey) {
455             print "$fileByKey{$missing}:$lineByKey{$missing}: missing key \"$missing\"\n";
456             $sawError = 1;
457         }
458
459         if ($sawError) {
460             print "\n$fileToUpdate:0: file is not up to date.\n";
461             exit 1;
462         }
463     }
464 } else {
465     print "error: $fileToUpdate does not exist\n";
466     exit 1;
467 }