[WTF] Move currentCPUTime and sleep(Seconds) to CPUTime.h and Seconds.h respectively
[WebKit-https.git] / Source / WebCore / extract-localizable-strings.pl
1 #!/usr/bin/env perl
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 warnings;
47 use File::Compare;
48 use File::Copy;
49 use FindBin;
50 use Getopt::Long;
51 use lib $FindBin::Bin;
52 use LocalizableStrings;
53 no warnings 'deprecated';
54
55 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 );
56
57 my $verify;
58 my $exceptionsFile;
59 my @directoriesToSkip = ();
60 my $treatWarningsAsErrors;
61
62 my %options = (
63     'verify' => \$verify,
64     'exceptions=s' => \$exceptionsFile,
65     'skip=s' => \@directoriesToSkip,
66     'treat-warnings-as-errors' => \$treatWarningsAsErrors,
67 );
68
69 GetOptions(%options);
70
71 setTreatWarningsAsErrors($treatWarningsAsErrors);
72
73 @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";
74
75 -f $exceptionsFile or die "Couldn't find exceptions file $exceptionsFile\n" unless !defined $exceptionsFile;
76
77 my $fileToUpdate = shift @ARGV;
78 -f $fileToUpdate or die "Couldn't find file to update $fileToUpdate\n";
79
80 my $warnAboutUnlocalizedStrings = defined $exceptionsFile;
81
82 my @directories = ();
83 if (@ARGV < 1) {
84     push(@directories, ".");
85 } else {
86     for my $dir (@ARGV) {
87         push @directories, $dir;
88     }
89 }
90
91 my $notLocalizedCount = 0;
92 my $NSLocalizeCount = 0;
93
94 my %exception;
95 my %usedException;
96
97 if (defined $exceptionsFile && open EXCEPTIONS, $exceptionsFile) {
98     while (<EXCEPTIONS>) {
99         chomp;
100         if (/^"([^\\"]|\\.)*"$/ or /^[-_\/\w\s.]+.(h|m|mm|c|cpp)$/ or /^[-_\/\w\s.]+.(h|m|mm|c|cpp):"([^\\"]|\\.)*"$/) {
101             if ($exception{$_}) {
102                 emitWarning($exceptionsFile, $., "exception for $_ appears twice");
103                 emitWarning($exceptionsFile, $exception{$_}, "first appearance");
104             } else {
105                 $exception{$_} = $.;
106             }
107         } else {
108             emitWarning($exceptionsFile, $., "syntax error");
109         }
110     }
111     close EXCEPTIONS;
112 }
113
114 my $quotedDirectoriesString = '"' . join('" "', @directories) . '"';
115 for my $dir (@directoriesToSkip) {
116     $quotedDirectoriesString .= ' -path "' . $dir . '" -prune -o';
117 }
118
119 my @files = ( split "\n", `find $quotedDirectoriesString \\( -name "*.h" -o -name "*.m" -o -name "*.mm" -o -name "*.c" -o -name "*.cpp" \\)` );
120
121 for my $file (sort @files) {
122     next if $file =~ /\/\w+LocalizableStrings\w*\.h$/ || $file =~ /\/LocalizedStrings\.h$/;
123
124     $file =~ s-^./--;
125
126     open SOURCE, $file or die "can't open $file\n";
127     
128     my $inComment = 0;
129     
130     my $expected = "";
131     my $macroLine;
132     my $macro;
133     my $UIString;
134     my $key;
135     my $comment;
136     my $mnemonic;
137     
138     my $string;
139     my $stringLine;
140     my $nestingLevel;
141     
142     my $previousToken = "";
143
144     while (<SOURCE>) {
145         chomp;
146         
147         # Handle continued multi-line comment.
148         if ($inComment) {
149             next unless s-.*\*/--;
150             $inComment = 0;
151         }
152
153         next unless defined $nestingLevel or /(\"|\/\*)/;
154     
155         # Handle all the tokens in the line.
156         while (s-^\s*([#\w]+|/\*|//|[^#\w/'"()\[\],]+|.)--) {
157             my $token = $1;
158
159             if ($token eq "@" and $expected and $expected eq "a quoted string") {
160                 next;
161             }
162
163             if ($token eq "\"") {
164                 if ($expected and $expected ne "a quoted string") {
165                     emitError($file, $., "found a quoted string but expected $expected");
166                     $expected = "";
167                 }
168                 if (s-^(([^\\$token]|\\.)*?)$token--) {
169                     if (!defined $string) {
170                         $stringLine = $.;
171                         $string = $1;
172                     } else {
173                         $string .= $1;
174                     }
175                 } else {
176                     emitError($file, $., "mismatched quotes");
177                     $_ = "";
178                 }
179                 next;
180             }
181             
182             if (defined $string) {
183 handleString:
184                 if ($expected) {
185                     if (!defined $UIString) {
186                         # FIXME: Validate UTF-8 here?
187                         $UIString = $string;
188                         $expected = ",";
189                     } elsif (($macro =~ /(WEB_)?UI_STRING_KEY(_INTERNAL)?$/) and !defined $key) {
190                         # FIXME: Validate UTF-8 here?
191                         $key = $string;
192                         $expected = ",";
193                     } elsif (($macro =~ /WEB_UI_STRING_WITH_MNEMONIC$/) and !defined $mnemonic) {
194                         $mnemonic = $string;
195                         $expected = ",";
196                     } elsif (!defined $comment) {
197                         # FIXME: Validate UTF-8 here?
198                         $comment = $string;
199                         $expected = ")";
200                     }
201                 } else {
202                     if (defined $nestingLevel) {
203                         # In a debug macro, no need to localize.
204                     } elsif ($previousToken eq "#include" or $previousToken eq "#import") {
205                         # File name, no need to localize.
206                     } elsif ($previousToken eq "extern" and $string eq "C") {
207                         # extern "C", no need to localize.
208                     } elsif ($string eq "") {
209                         # Empty string can sometimes be localized, but we need not complain if not.
210                     } elsif ($exception{$file}) {
211                         $usedException{$file} = 1;
212                     } elsif ($exception{"\"$string\""}) {
213                         $usedException{"\"$string\""} = 1;
214                     } elsif ($exception{"$file:\"$string\""}) {
215                         $usedException{"$file:\"$string\""} = 1;
216                     } else {
217                         emitWarning($file, $stringLine, "\"$string\" is not marked for localization") if $warnAboutUnlocalizedStrings;
218                         $notLocalizedCount++;
219                     }
220                 }
221                 $string = undef;
222                 last if !defined $token;
223             }
224             
225             $previousToken = $token;
226
227             if ($token =~ /^NSLocalized/ && $token !~ /NSLocalizedDescriptionKey/ && $token !~ /NSLocalizedStringFromTableInBundle/ && $token !~ /NSLocalizedFileSizeDescription/ && $token !~ /NSLocalizedDescriptionKey/ && $token !~ /NSLocalizedRecoverySuggestionErrorKey/) {
228                 emitError($file, $., "found a use of an NSLocalized macro ($token); not supported");
229                 $nestingLevel = 0 if !defined $nestingLevel;
230                 $NSLocalizeCount++;
231             } elsif ($token eq "/*") {
232                 if (!s-^.*?\*/--) {
233                     $_ = ""; # If the comment doesn't end, discard the result of the line and set flag
234                     $inComment = 1;
235                 }
236             } elsif ($token eq "//") {
237                 $_ = ""; # Discard the rest of the line
238             } elsif ($token eq "'") {
239                 if (!s-([^\\]|\\.)'--) { #' <-- that single quote makes the Project Builder editor less confused
240                     emitError($file, $., "mismatched single quote");
241                     $_ = "";
242                 }
243             } else {
244                 if ($expected and $expected ne $token) {
245                     emitError($file, $., "found $token but expected $expected");
246                     $expected = "";
247                 }
248                 if (($token =~ /(WEB_)?UI_STRING(_KEY)?(_INTERNAL)?$/) || ($token =~ /WEB_UI_NSSTRING$/) || ($token =~ /WEB_UI_STRING_WITH_MNEMONIC$/) || ($token =~ /WEB_UI_CFSTRING$/)) {
249                     $expected = "(";
250                     $macro = $token;
251                     $UIString = undef;
252                     $key = undef;
253                     $comment = undef;
254                     $mnemonic = undef;
255                     $macroLine = $.;
256                 } elsif ($token eq "(" or $token eq "[") {
257                     ++$nestingLevel if defined $nestingLevel;
258                     $expected = "a quoted string" if $expected;
259                 } elsif ($token eq ",") {
260                     $expected = "a quoted string" if $expected;
261                 } elsif ($token eq ")" or $token eq "]") {
262                     $nestingLevel = undef if defined $nestingLevel && !--$nestingLevel;
263                     if ($expected) {
264                         $key = $UIString if !defined $key;
265                         HandleUIString($UIString, $key, $comment, $file, $macroLine);
266                         $macro = "";
267                         $expected = "";
268                     }
269                 } elsif ($isDebugMacro{$token}) {
270                     $nestingLevel = 0 if !defined $nestingLevel;
271                 }
272             }
273         }
274             
275     }
276     
277     goto handleString if defined $string;
278     
279     if ($expected) {
280         emitError($file, 0, "reached end of file but expected $expected");
281     }
282     
283     close SOURCE;
284 }
285
286 print "\n" if sawError() || $notLocalizedCount || $NSLocalizeCount;
287
288 my @unusedExceptions = sort grep { !$usedException{$_} } keys %exception;
289 if (@unusedExceptions) {
290     for my $unused (@unusedExceptions) {
291         emitWarning($exceptionsFile, $exception{$unused}, "exception $unused not used");
292     }
293     print "\n";
294 }
295
296 print localizedCount() . " localizable strings\n" if localizedCount();
297 print keyCollisionCount() . " key collisions\n" if keyCollisionCount();
298 print "$notLocalizedCount strings not marked for localization\n" if $notLocalizedCount;
299 print "$NSLocalizeCount uses of NSLocalize\n" if $NSLocalizeCount;
300 print scalar(@unusedExceptions), " unused exceptions\n" if @unusedExceptions;
301
302 if (sawError()) {
303     print "\nErrors encountered. Exiting without writing to $fileToUpdate.\n";
304     exit 1;
305 }
306
307 if (-e "$fileToUpdate") {
308     if (!$verify) {
309         my $temporaryFile = "$fileToUpdate.updated";
310         writeStringsFile($temporaryFile);
311
312         # Avoid updating the target file's modification time if the contents have not changed.
313         if (compare($temporaryFile, $fileToUpdate)) {
314             move($temporaryFile, $fileToUpdate);
315         } else {
316             unlink $temporaryFile;
317         }
318     } else {
319         verifyStringsFile($fileToUpdate);
320     }
321 } else {
322     print "error: $fileToUpdate does not exist\n";
323     exit 1;
324 }