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