Make it possible to use WEB_UI_STRING macros to mark translatable strings in glib...
[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     my $mnemonic;
136     
137     my $string;
138     my $stringLine;
139     my $nestingLevel;
140     
141     my $previousToken = "";
142
143     while (<SOURCE>) {
144         chomp;
145         
146         # Handle continued multi-line comment.
147         if ($inComment) {
148             next unless s-.*\*/--;
149             $inComment = 0;
150         }
151
152         next unless defined $nestingLevel or /(\"|\/\*)/;
153     
154         # Handle all the tokens in the line.
155         while (s-^\s*([#\w]+|/\*|//|[^#\w/'"()\[\],]+|.)--) {
156             my $token = $1;
157
158             if ($token eq "@" and $expected and $expected eq "a quoted string") {
159                 next;
160             }
161
162             if ($token eq "\"") {
163                 if ($expected and $expected ne "a quoted string") {
164                     emitError($file, $., "found a quoted string but expected $expected");
165                     $expected = "";
166                 }
167                 if (s-^(([^\\$token]|\\.)*?)$token--) {
168                     if (!defined $string) {
169                         $stringLine = $.;
170                         $string = $1;
171                     } else {
172                         $string .= $1;
173                     }
174                 } else {
175                     emitError($file, $., "mismatched quotes");
176                     $_ = "";
177                 }
178                 next;
179             }
180             
181             if (defined $string) {
182 handleString:
183                 if ($expected) {
184                     if (!defined $UIString) {
185                         # FIXME: Validate UTF-8 here?
186                         $UIString = $string;
187                         $expected = ",";
188                     } elsif (($macro =~ /(WEB_)?UI_STRING_KEY(_INTERNAL)?$/) and !defined $key) {
189                         # FIXME: Validate UTF-8 here?
190                         $key = $string;
191                         $expected = ",";
192                     } elsif (($macro =~ /WEB_UI_STRING_WITH_MNEMONIC$/) and !defined $mnemonic) {
193                         $mnemonic = $string;
194                         $expected = ",";
195                     } elsif (!defined $comment) {
196                         # FIXME: Validate UTF-8 here?
197                         $comment = $string;
198                         $expected = ")";
199                     }
200                 } else {
201                     if (defined $nestingLevel) {
202                         # In a debug macro, no need to localize.
203                     } elsif ($previousToken eq "#include" or $previousToken eq "#import") {
204                         # File name, no need to localize.
205                     } elsif ($previousToken eq "extern" and $string eq "C") {
206                         # extern "C", no need to localize.
207                     } elsif ($string eq "") {
208                         # Empty string can sometimes be localized, but we need not complain if not.
209                     } elsif ($exception{$file}) {
210                         $usedException{$file} = 1;
211                     } elsif ($exception{"\"$string\""}) {
212                         $usedException{"\"$string\""} = 1;
213                     } elsif ($exception{"$file:\"$string\""}) {
214                         $usedException{"$file:\"$string\""} = 1;
215                     } else {
216                         emitWarning($file, $stringLine, "\"$string\" is not marked for localization") if $warnAboutUnlocalizedStrings;
217                         $notLocalizedCount++;
218                     }
219                 }
220                 $string = undef;
221                 last if !defined $token;
222             }
223             
224             $previousToken = $token;
225
226             if ($token =~ /^NSLocalized/ && $token !~ /NSLocalizedDescriptionKey/ && $token !~ /NSLocalizedStringFromTableInBundle/ && $token !~ /NSLocalizedFileSizeDescription/ && $token !~ /NSLocalizedDescriptionKey/ && $token !~ /NSLocalizedRecoverySuggestionErrorKey/) {
227                 emitError($file, $., "found a use of an NSLocalized macro ($token); not supported");
228                 $nestingLevel = 0 if !defined $nestingLevel;
229                 $NSLocalizeCount++;
230             } elsif ($token eq "/*") {
231                 if (!s-^.*?\*/--) {
232                     $_ = ""; # If the comment doesn't end, discard the result of the line and set flag
233                     $inComment = 1;
234                 }
235             } elsif ($token eq "//") {
236                 $_ = ""; # Discard the rest of the line
237             } elsif ($token eq "'") {
238                 if (!s-([^\\]|\\.)'--) { #' <-- that single quote makes the Project Builder editor less confused
239                     emitError($file, $., "mismatched single quote");
240                     $_ = "";
241                 }
242             } else {
243                 if ($expected and $expected ne $token) {
244                     emitError($file, $., "found $token but expected $expected");
245                     $expected = "";
246                 }
247                 if (($token =~ /(WEB_)?UI_STRING(_KEY)?(_INTERNAL)?$/) || ($token =~ /WEB_UI_NSSTRING$/) || ($token =~ /WEB_UI_STRING_WITH_MNEMONIC$/) || ($token =~ /WEB_UI_CFSTRING$/)) {
248                     $expected = "(";
249                     $macro = $token;
250                     $UIString = undef;
251                     $key = undef;
252                     $comment = undef;
253                     $mnemonic = undef;
254                     $macroLine = $.;
255                 } elsif ($token eq "(" or $token eq "[") {
256                     ++$nestingLevel if defined $nestingLevel;
257                     $expected = "a quoted string" if $expected;
258                 } elsif ($token eq ",") {
259                     $expected = "a quoted string" if $expected;
260                 } elsif ($token eq ")" or $token eq "]") {
261                     $nestingLevel = undef if defined $nestingLevel && !--$nestingLevel;
262                     if ($expected) {
263                         $key = $UIString if !defined $key;
264                         HandleUIString($UIString, $key, $comment, $file, $macroLine);
265                         $macro = "";
266                         $expected = "";
267                     }
268                 } elsif ($isDebugMacro{$token}) {
269                     $nestingLevel = 0 if !defined $nestingLevel;
270                 }
271             }
272         }
273             
274     }
275     
276     goto handleString if defined $string;
277     
278     if ($expected) {
279         emitError($file, 0, "reached end of file but expected $expected");
280     }
281     
282     close SOURCE;
283 }
284
285 print "\n" if sawError() || $notLocalizedCount || $NSLocalizeCount;
286
287 my @unusedExceptions = sort grep { !$usedException{$_} } keys %exception;
288 if (@unusedExceptions) {
289     for my $unused (@unusedExceptions) {
290         emitWarning($exceptionsFile, $exception{$unused}, "exception $unused not used");
291     }
292     print "\n";
293 }
294
295 print localizedCount() . " localizable strings\n" if localizedCount();
296 print keyCollisionCount() . " key collisions\n" if keyCollisionCount();
297 print "$notLocalizedCount strings not marked for localization\n" if $notLocalizedCount;
298 print "$NSLocalizeCount uses of NSLocalize\n" if $NSLocalizeCount;
299 print scalar(@unusedExceptions), " unused exceptions\n" if @unusedExceptions;
300
301 if (sawError()) {
302     print "\nErrors encountered. Exiting without writing to $fileToUpdate.\n";
303     exit 1;
304 }
305
306 if (-e "$fileToUpdate") {
307     if (!$verify) {
308         my $temporaryFile = "$fileToUpdate.updated";
309         writeStringsFile($temporaryFile);
310
311         # Avoid updating the target file's modification time if the contents have not changed.
312         if (compare($temporaryFile, $fileToUpdate)) {
313             move($temporaryFile, $fileToUpdate);
314         } else {
315             unlink $temporaryFile;
316         }
317     } else {
318         verifyStringsFile($fileToUpdate);
319     }
320 } else {
321     print "error: $fileToUpdate does not exist\n";
322     exit 1;
323 }